From f0beaafa9672f7dba7be7ec4837a40b0b82fe3a5 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 11 May 2026 14:33:08 -0600 Subject: [PATCH 1/5] feat: Adds `Data` sequence support to Server and Client It also changes the error pathway to close immediately instead of sending a formatted message. --- Sources/GraphQLWS/Client.swift | 143 +++++++++--------- Sources/GraphQLWS/Server.swift | 110 +++++++------- Tests/GraphQLWSTests/GraphQLWSTests.swift | 64 +++----- .../GraphQLWSTests/Utils/TestMessenger.swift | 17 ++- 4 files changed, 166 insertions(+), 168 deletions(-) diff --git a/Sources/GraphQLWS/Client.swift b/Sources/GraphQLWS/Client.swift index 82656e4..7e3892b 100644 --- a/Sources/GraphQLWS/Client.swift +++ b/Sources/GraphQLWS/Client.swift @@ -51,83 +51,90 @@ public actor Client { /// Listen and react to the provided async sequence of server messages. This function will block until the stream is completed. /// - Parameter incoming: The server message sequence that the client should react to. public func listen(to incoming: A) async throws - where A.Element == String { + where A.Element == Data { for try await message in incoming { - // Detect and ignore error responses. - if message.starts(with: "44") { - // TODO: Determine what to do with returned error messages + try await respond(to: message) + } + } + + /// Listen and react to the provided async sequence of server messages. This function will block until the stream is completed. + /// - Parameter incoming: The server message sequence that the client should react to. + @available(*, deprecated, message: "Use `Data` sequence instead.") + public func listen(to incoming: A) async throws + where A.Element == String { + for try await stringMessage in incoming { + guard let message = stringMessage.data(using: .utf8) else { + try await self.error(.invalidEncoding()) return } + try await respond(to: message) + } + } - guard let json = message.data(using: .utf8) else { - try await error(.invalidEncoding()) + private func respond(to message: Data) async throws { + let response: Response + do { + response = try decoder.decode(Response.self, from: message) + } catch { + try await self.error(.noType()) + return + } + + switch response.type { + case .GQL_CONNECTION_ERROR: + guard + let connectionErrorResponse = try? decoder.decode( + ConnectionErrorResponse.self, + from: message + ) + else { + try await error(.invalidResponseFormat(messageType: .GQL_CONNECTION_ERROR)) return } - - let response: Response - do { - response = try decoder.decode(Response.self, from: json) - } catch { - try await self.error(.noType()) + try await onConnectionError(connectionErrorResponse, self) + case .GQL_CONNECTION_ACK: + guard + let connectionAckResponse = try? decoder.decode( + ConnectionAckResponse.self, + from: message + ) + else { + try await error(.invalidResponseFormat(messageType: .GQL_CONNECTION_ERROR)) return } - - switch response.type { - case .GQL_CONNECTION_ERROR: - guard - let connectionErrorResponse = try? decoder.decode( - ConnectionErrorResponse.self, - from: json - ) - else { - try await error(.invalidResponseFormat(messageType: .GQL_CONNECTION_ERROR)) - return - } - try await onConnectionError(connectionErrorResponse, self) - case .GQL_CONNECTION_ACK: - guard - let connectionAckResponse = try? decoder.decode( - ConnectionAckResponse.self, - from: json - ) - else { - try await error(.invalidResponseFormat(messageType: .GQL_CONNECTION_ERROR)) - return - } - try await onConnectionAck(connectionAckResponse, self) - case .GQL_CONNECTION_KEEP_ALIVE: - guard - let connectionKeepAliveResponse = try? decoder.decode( - ConnectionKeepAliveResponse.self, - from: json - ) - else { - try await error(.invalidResponseFormat(messageType: .GQL_CONNECTION_KEEP_ALIVE)) - return - } - try await onConnectionKeepAlive(connectionKeepAliveResponse, self) - case .GQL_DATA: - guard let nextResponse = try? decoder.decode(DataResponse.self, from: json) else { - try await error(.invalidResponseFormat(messageType: .GQL_DATA)) - return - } - try await onData(nextResponse, self) - case .GQL_ERROR: - guard let errorResponse = try? decoder.decode(ErrorResponse.self, from: json) else { - try await error(.invalidResponseFormat(messageType: .GQL_ERROR)) - return - } - try await onError(errorResponse, self) - case .GQL_COMPLETE: - guard let completeResponse = try? decoder.decode(CompleteResponse.self, from: json) - else { - try await error(.invalidResponseFormat(messageType: .GQL_COMPLETE)) - return - } - try await onComplete(completeResponse, self) - default: - try await error(.invalidType()) + try await onConnectionAck(connectionAckResponse, self) + case .GQL_CONNECTION_KEEP_ALIVE: + guard + let connectionKeepAliveResponse = try? decoder.decode( + ConnectionKeepAliveResponse.self, + from: message + ) + else { + try await error(.invalidResponseFormat(messageType: .GQL_CONNECTION_KEEP_ALIVE)) + return + } + try await onConnectionKeepAlive(connectionKeepAliveResponse, self) + case .GQL_DATA: + guard let nextResponse = try? decoder.decode(DataResponse.self, from: message) else { + try await error(.invalidResponseFormat(messageType: .GQL_DATA)) + return + } + try await onData(nextResponse, self) + case .GQL_ERROR: + guard let errorResponse = try? decoder.decode(ErrorResponse.self, from: message) else { + try await error(.invalidResponseFormat(messageType: .GQL_ERROR)) + return + } + try await onError(errorResponse, self) + case .GQL_COMPLETE: + guard let completeResponse = try? decoder.decode(CompleteResponse.self, from: message) + else { + try await error(.invalidResponseFormat(messageType: .GQL_COMPLETE)) + return } + try await onComplete(completeResponse, self) + default: + try await error(.invalidType()) } } diff --git a/Sources/GraphQLWS/Server.swift b/Sources/GraphQLWS/Server.swift index a04ef6b..284fe04 100644 --- a/Sources/GraphQLWS/Server.swift +++ b/Sources/GraphQLWS/Server.swift @@ -56,66 +56,74 @@ where /// Listen and react to the provided async sequence of client messages. This function will block until the stream is completed. /// - Parameter incoming: The client message sequence that the server should react to. public func listen(to incoming: A) async throws - where A.Element == String { + where A.Element == Data { for try await message in incoming { - // Detect and ignore error responses. - if message.starts(with: "44") { - // TODO: Determine what to do with returned error messages - return - } + try await respond(to: message) + } + } - guard let json = message.data(using: .utf8) else { + /// Listen and react to the provided async sequence of client messages. This function will block until the stream is completed. + /// - Parameter incoming: The client message sequence that the server should react to. + @available(*, deprecated, message: "Use `Data` sequence instead.") + public func listen(to incoming: A) async throws + where A.Element == String { + for try await stringMessage in incoming { + guard let message = stringMessage.data(using: .utf8) else { try await error(.invalidEncoding()) return } - let request: Request - do { - request = try decoder.decode(Request.self, from: json) - } catch { - try await self.error(.noType()) + try await respond(to: message) + } + } + + private func respond(to message: Data) async throws { + let request: Request + do { + request = try decoder.decode(Request.self, from: message) + } catch { + try await self.error(.noType()) + return + } + + // handle incoming message + switch request.type { + case .GQL_CONNECTION_INIT: + guard + let connectionInitRequest = try? decoder.decode( + ConnectionInitRequest.self, + from: message + ) + else { + try await error(.invalidRequestFormat(messageType: .GQL_CONNECTION_INIT)) return } - - // handle incoming message - switch request.type { - case .GQL_CONNECTION_INIT: - guard - let connectionInitRequest = try? decoder.decode( - ConnectionInitRequest.self, - from: json - ) - else { - try await error(.invalidRequestFormat(messageType: .GQL_CONNECTION_INIT)) - return - } - try await onConnectionInit(connectionInitRequest, messenger) - case .GQL_START: - guard let startRequest = try? decoder.decode(StartRequest.self, from: json) else { - try await error(.invalidRequestFormat(messageType: .GQL_START)) - return - } - try await onStart(startRequest, messenger) - case .GQL_STOP: - guard let stopRequest = try? decoder.decode(StopRequest.self, from: json) else { - try await error(.invalidRequestFormat(messageType: .GQL_STOP)) - return - } - try await onStop(stopRequest) - case .GQL_CONNECTION_TERMINATE: - guard - let connectionTerminateRequest = try? decoder.decode( - ConnectionTerminateRequest.self, - from: json - ) - else { - try await error(.invalidRequestFormat(messageType: .GQL_CONNECTION_TERMINATE)) - return - } - try await onConnectionTerminate(connectionTerminateRequest, messenger) - default: - try await error(.invalidType()) + try await onConnectionInit(connectionInitRequest, messenger) + case .GQL_START: + guard let startRequest = try? decoder.decode(StartRequest.self, from: message) else { + try await error(.invalidRequestFormat(messageType: .GQL_START)) + return + } + try await onStart(startRequest, messenger) + case .GQL_STOP: + guard let stopRequest = try? decoder.decode(StopRequest.self, from: message) else { + try await error(.invalidRequestFormat(messageType: .GQL_STOP)) + return + } + try await onStop(stopRequest) + case .GQL_CONNECTION_TERMINATE: + guard + let connectionTerminateRequest = try? decoder.decode( + ConnectionTerminateRequest.self, + from: message + ) + else { + try await error(.invalidRequestFormat(messageType: .GQL_CONNECTION_TERMINATE)) + return } + try await onConnectionTerminate(connectionTerminateRequest, messenger) + default: + try await error(.invalidType()) } } diff --git a/Tests/GraphQLWSTests/GraphQLWSTests.swift b/Tests/GraphQLWSTests/GraphQLWSTests.swift index 11ccb58..1fe244b 100644 --- a/Tests/GraphQLWSTests/GraphQLWSTests.swift +++ b/Tests/GraphQLWSTests/GraphQLWSTests.swift @@ -28,21 +28,7 @@ struct GraphqlTransportWSTests { ).get() } ) - let (messageStream, messageContinuation) = AsyncThrowingStream - .makeStream() - let serverMessageStream = serverMessenger.stream.map { message in - messageContinuation.yield(message) - // Expect only one message - messageContinuation.finish() - return message - } - let client = Client( - messenger: clientMessenger, - onError: { message, _ in - messageContinuation.finish(throwing: message.payload[0]) - await clientMessenger.close() - } - ) + let client = Client(messenger: clientMessenger) let clientStream = clientMessenger.stream Task { try await server.listen(to: clientStream) @@ -59,13 +45,16 @@ struct GraphqlTransportWSTests { ), id: UUID().uuidString ) - try await client.listen(to: serverMessageStream) - let messages = try await messageStream.reduce(into: [String]()) { result, message in - result.append(message) + let error = await #expect(throws: TestMessengerError.self) { + try await client.listen(to: serverMessenger.stream) } #expect( - messages == ["\(ErrorCode.notInitialized): Connection not initialized"] + error + == TestMessengerError( + code: ErrorCode.notInitialized.rawValue, + message: "Connection not initialized" + ) ) } @@ -91,21 +80,7 @@ struct GraphqlTransportWSTests { ).get() } ) - let (messageStream, messageContinuation) = AsyncThrowingStream - .makeStream() - let serverMessageStream = serverMessenger.stream.map { message in - messageContinuation.yield(message) - // Expect only one message - messageContinuation.finish() - return message - } - let client = Client( - messenger: clientMessenger, - onError: { message, _ in - messageContinuation.finish(throwing: message.payload[0]) - await clientMessenger.close() - } - ) + let client = Client(messenger: clientMessenger) let clientStream = clientMessenger.stream Task { try await server.listen(to: clientStream) @@ -117,13 +92,16 @@ struct GraphqlTransportWSTests { authToken: "" ) ) - try await client.listen(to: serverMessageStream) - let messages = try await messageStream.reduce(into: [String]()) { result, message in - result.append(message) + let error = await #expect(throws: TestMessengerError.self) { + try await client.listen(to: serverMessenger.stream) } #expect( - messages == ["\(ErrorCode.unauthorized): Unauthorized"] + error + == TestMessengerError( + code: ErrorCode.unauthorized.rawValue, + message: "Unauthorized" + ) ) } @@ -149,8 +127,7 @@ struct GraphqlTransportWSTests { ).get() } ) - let (messageStream, messageContinuation) = AsyncThrowingStream - .makeStream() + let (messageStream, messageContinuation) = AsyncThrowingStream.makeStream() let serverMessageStream = serverMessenger.stream.map { message in messageContinuation.yield(message) return message @@ -187,7 +164,7 @@ struct GraphqlTransportWSTests { try await client.sendConnectionInit(payload: TokenInitPayload(authToken: "")) try await client.listen(to: serverMessageStream) - let messages = try await messageStream.reduce(into: [String]()) { result, message in + let messages = try await messageStream.reduce(into: [Data]()) { result, message in result.append(message) } #expect( @@ -223,8 +200,7 @@ struct GraphqlTransportWSTests { return subscription } ) - let (messageStream, messageContinuation) = AsyncThrowingStream - .makeStream() + let (messageStream, messageContinuation) = AsyncThrowingStream.makeStream() // Used to extract the server messages let serverMessageStream = serverMessenger.stream.map { message in messageContinuation.yield(message) @@ -274,7 +250,7 @@ struct GraphqlTransportWSTests { try await client.sendConnectionInit(payload: TokenInitPayload(authToken: "")) try await client.listen(to: serverMessageStream) - let messages = try await messageStream.reduce(into: [String]()) { result, message in + let messages = try await messageStream.reduce(into: [Data]()) { result, message in result.append(message) } #expect( diff --git a/Tests/GraphQLWSTests/Utils/TestMessenger.swift b/Tests/GraphQLWSTests/Utils/TestMessenger.swift index ac86b02..e1ea713 100644 --- a/Tests/GraphQLWSTests/Utils/TestMessenger.swift +++ b/Tests/GraphQLWSTests/Utils/TestMessenger.swift @@ -5,21 +5,23 @@ import Foundation /// Messenger for simple testing that doesn't require starting up a websocket server. actor TestMessenger: Messenger { /// An async stream of the messages sent through this messenger. - let stream: AsyncStream - private var continuation: AsyncStream.Continuation + let stream: AsyncThrowingStream + private var continuation: AsyncThrowingStream.Continuation init() { - let (stream, continuation) = AsyncStream.makeStream() + let (stream, continuation) = AsyncThrowingStream.makeStream() self.stream = stream self.continuation = continuation } func send(_ message: S) async throws where S.Element == Character { - continuation.yield(String(message)) + if let data = String(message).data(using: .utf8) { + continuation.yield(data) + } } func error(_ message: String, code: Int) async throws { - continuation.yield("\(code): \(message)") + continuation.finish(throwing: TestMessengerError(code: code, message: message)) continuation.finish() } @@ -27,3 +29,8 @@ actor TestMessenger: Messenger { continuation.finish() } } + +struct TestMessengerError: Error, Equatable { + let code: Int + let message: String +} From 898f559f99ebe7d4f4780bd51fe1c359aedf3a30 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 11 May 2026 14:58:15 -0600 Subject: [PATCH 2/5] feat: Adds `Data` support to Messenger --- Sources/GraphQLWS/Client.swift | 30 ++++++++++++++--------- Sources/GraphQLWS/JsonEncodable.swift | 23 ------------------ Sources/GraphQLWS/Messenger.swift | 12 ++++++++++ Sources/GraphQLWS/Requests.swift | 10 ++++---- Sources/GraphQLWS/Responses.swift | 16 ++++++------- Sources/GraphQLWS/Server.swift | 34 ++++++++++++++++----------- 6 files changed, 64 insertions(+), 61 deletions(-) delete mode 100644 Sources/GraphQLWS/JsonEncodable.swift diff --git a/Sources/GraphQLWS/Client.swift b/Sources/GraphQLWS/Client.swift index 7e3892b..9904698 100644 --- a/Sources/GraphQLWS/Client.swift +++ b/Sources/GraphQLWS/Client.swift @@ -141,35 +141,43 @@ public actor Client { /// Send a `connection_init` request through the messenger public func sendConnectionInit(payload: InitPayload) async throws { try await messenger.send( - ConnectionInitRequest( - payload: payload - ).toJSON(encoder) + encoder.encode( + ConnectionInitRequest( + payload: payload + ) + ) ) } /// Send a `start` request through the messenger public func sendStart(payload: GraphQLRequest, id: String) async throws { try await messenger.send( - StartRequest( - payload: payload, - id: id - ).toJSON(encoder) + encoder.encode( + StartRequest( + payload: payload, + id: id + ) + ) ) } /// Send a `stop` request through the messenger public func sendStop(id: String) async throws { try await messenger.send( - StopRequest( - id: id - ).toJSON(encoder) + encoder.encode( + StopRequest( + id: id + ) + ) ) } /// Send a `connection_terminate` request through the messenger public func sendConnectionTerminate() async throws { try await messenger.send( - ConnectionTerminateRequest().toJSON(encoder) + encoder.encode( + ConnectionTerminateRequest() + ) ) } diff --git a/Sources/GraphQLWS/JsonEncodable.swift b/Sources/GraphQLWS/JsonEncodable.swift deleted file mode 100644 index 911d14f..0000000 --- a/Sources/GraphQLWS/JsonEncodable.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import GraphQL - -/// Indicates an object that can be converted into JSON for websocket messaging -protocol JsonEncodable: Codable {} - -extension JsonEncodable { - /// Converts the object into a JSON string - /// - Parameter encoder: JSON Encoder used to encode the object into a string - /// - Returns: The JSON string representation of the object, or an error JSON if not possible - func toJSON(_ encoder: GraphQLJSONEncoder) -> String { - let data: Data - do { - data = try encoder.encode(self) - } catch { - return EncodingErrorResponse("Unable to encode response").toJSON(encoder) - } - guard let body = String(data: data, encoding: .utf8) else { - return EncodingErrorResponse("Encoded response can't be cast to string").toJSON(encoder) - } - return body - } -} diff --git a/Sources/GraphQLWS/Messenger.swift b/Sources/GraphQLWS/Messenger.swift index e0ba6d9..ef45d77 100644 --- a/Sources/GraphQLWS/Messenger.swift +++ b/Sources/GraphQLWS/Messenger.swift @@ -15,3 +15,15 @@ public protocol Messenger: Sendable { /// - code: An error code func error(_ message: String, code: Int) async throws } + +extension Messenger { + /// Send a message through this messenger + /// - Parameter message: The message to send + func send(_ message: Data) async throws { + // TODO: Ideally Data is our native interface, and String is the extension. + // Since that change is breaking, we will do it on the next major version. + if let string = String(data: message, encoding: .utf8) { + try await send(string) + } + } +} diff --git a/Sources/GraphQLWS/Requests.swift b/Sources/GraphQLWS/Requests.swift index 251df45..b77d384 100644 --- a/Sources/GraphQLWS/Requests.swift +++ b/Sources/GraphQLWS/Requests.swift @@ -2,12 +2,12 @@ import Foundation import GraphQL /// A general request. This object's type is used to triage to other, more specific request objects. -public struct Request: Equatable, JsonEncodable { +public struct Request: Equatable, Codable { public let type: RequestMessageType } /// A websocket `connection_init` request from the client to the server -public struct ConnectionInitRequest: Equatable, JsonEncodable { +public struct ConnectionInitRequest: Equatable, Codable { public let type: RequestMessageType = .GQL_CONNECTION_INIT public let payload: InitPayload @@ -31,7 +31,7 @@ public struct ConnectionInitRequest: Equatable } /// A websocket `start` request from the client to the server -public struct StartRequest: Equatable, JsonEncodable { +public struct StartRequest: Equatable, Codable { public let type: RequestMessageType = .GQL_START public let payload: GraphQLRequest public let id: String @@ -57,7 +57,7 @@ public struct StartRequest: Equatable, JsonEncodable { } /// A websocket `stop` request from the client to the server -public struct StopRequest: Equatable, JsonEncodable { +public struct StopRequest: Equatable, Codable { public let type: RequestMessageType = .GQL_STOP public let id: String @@ -81,7 +81,7 @@ public struct StopRequest: Equatable, JsonEncodable { } /// A websocket `connection_terminate` request from the client to the server -public struct ConnectionTerminateRequest: Equatable, JsonEncodable { +public struct ConnectionTerminateRequest: Equatable, Codable { public let type: RequestMessageType = .GQL_CONNECTION_TERMINATE public init() {} diff --git a/Sources/GraphQLWS/Responses.swift b/Sources/GraphQLWS/Responses.swift index c914eb2..d1901d0 100644 --- a/Sources/GraphQLWS/Responses.swift +++ b/Sources/GraphQLWS/Responses.swift @@ -2,12 +2,12 @@ import Foundation import GraphQL /// A general response. This object's type is used to triage to other, more specific response objects. -public struct Response: Equatable, JsonEncodable { +public struct Response: Equatable, Codable { public let type: ResponseMessageType } /// A websocket `connection_ack` response from the server to the client -public struct ConnectionAckResponse: Equatable, JsonEncodable { +public struct ConnectionAckResponse: Equatable, Codable { public let type: ResponseMessageType = .GQL_CONNECTION_ACK public let payload: [String: Map]? @@ -31,7 +31,7 @@ public struct ConnectionAckResponse: Equatable, JsonEncodable { } /// A websocket `connection_error` response from the server to the client -public struct ConnectionErrorResponse: Equatable, JsonEncodable { +public struct ConnectionErrorResponse: Equatable, Codable { public let type: ResponseMessageType = .GQL_CONNECTION_ERROR public let payload: [String: Map]? @@ -55,7 +55,7 @@ public struct ConnectionErrorResponse: Equatable, JsonEncodable { } /// A websocket `ka` response from the server to the client -public struct ConnectionKeepAliveResponse: Equatable, JsonEncodable { +public struct ConnectionKeepAliveResponse: Equatable, Codable { public let type: ResponseMessageType = .GQL_CONNECTION_KEEP_ALIVE public let payload: [String: Map]? @@ -81,7 +81,7 @@ public struct ConnectionKeepAliveResponse: Equatable, JsonEncodable { } /// A websocket `data` response from the server to the client -public struct DataResponse: Equatable, JsonEncodable { +public struct DataResponse: Equatable, Codable { public let type: ResponseMessageType = .GQL_DATA public let payload: GraphQLResult? public let id: String @@ -107,7 +107,7 @@ public struct DataResponse: Equatable, JsonEncodable { } /// A websocket `complete` response from the server to the client -public struct CompleteResponse: Equatable, JsonEncodable { +public struct CompleteResponse: Equatable, Codable { public let type: ResponseMessageType = .GQL_COMPLETE public let id: String @@ -130,7 +130,7 @@ public struct CompleteResponse: Equatable, JsonEncodable { } /// A websocket `error` response from the server to the client -public struct ErrorResponse: Equatable, JsonEncodable { +public struct ErrorResponse: Equatable, Codable { public let type: ResponseMessageType = .GQL_ERROR public let payload: [GraphQLError] public let id: String @@ -203,7 +203,7 @@ public struct ResponseMessageType: Equatable, Codable, Sendable { /// A websocket `error` response from the server to the client that indicates an issue with encoding /// a response JSON -struct EncodingErrorResponse: Equatable, Codable, JsonEncodable { +struct EncodingErrorResponse: Equatable, Codable { let type: ResponseMessageType let payload: [String: String] diff --git a/Sources/GraphQLWS/Server.swift b/Sources/GraphQLWS/Server.swift index 284fe04..5b297a7 100644 --- a/Sources/GraphQLWS/Server.swift +++ b/Sources/GraphQLWS/Server.swift @@ -226,40 +226,44 @@ where /// Send a `connection_ack` response through the messenger private func sendConnectionAck(_ payload: [String: Map]? = nil) async throws { try await messenger.send( - ConnectionAckResponse(payload: payload).toJSON(encoder) + encoder.encode(ConnectionAckResponse(payload: payload)) ) } /// Send a `connection_error` response through the messenger private func sendConnectionError(_ payload: [String: Map]? = nil) async throws { try await messenger.send( - ConnectionErrorResponse(payload: payload).toJSON(encoder) + encoder.encode(ConnectionErrorResponse(payload: payload)) ) } /// Send a `ka` response through the messenger private func sendConnectionKeepAlive(_ payload: [String: Map]? = nil) async throws { try await messenger.send( - ConnectionKeepAliveResponse(payload: payload).toJSON(encoder) + encoder.encode(ConnectionKeepAliveResponse(payload: payload)) ) } /// Send a `data` response through the messenger private func sendData(_ payload: GraphQLResult? = nil, id: String) async throws { try await messenger.send( - DataResponse( - payload: payload, - id: id - ).toJSON(encoder) + encoder.encode( + DataResponse( + payload: payload, + id: id + ) + ) ) } /// Send a `complete` response through the messenger private func sendComplete(id: String) async throws { try await messenger.send( - CompleteResponse( - id: id - ).toJSON(encoder) + encoder.encode( + CompleteResponse( + id: id + ) + ) ) try await onOperationComplete(id) } @@ -267,10 +271,12 @@ where /// Send an `error` response through the messenger private func sendError(_ errors: [Error], id: String) async throws { try await messenger.send( - ErrorResponse( - errors, - id: id - ).toJSON(encoder) + encoder.encode( + ErrorResponse( + errors, + id: id + ) + ) ) try await onOperationError(id, errors) } From 99fcd3941ca920592a13097282ff3bb6caae319d Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 12 May 2026 19:16:31 -0600 Subject: [PATCH 3/5] feat!: Messenger uses Data --- Sources/GraphQLWS/Messenger.swift | 14 +------------- Sources/GraphQLWS/Server.swift | 8 ++++---- Tests/GraphQLWSTests/Utils/TestMessenger.swift | 6 ++---- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/Sources/GraphQLWS/Messenger.swift b/Sources/GraphQLWS/Messenger.swift index ef45d77..543c6fa 100644 --- a/Sources/GraphQLWS/Messenger.swift +++ b/Sources/GraphQLWS/Messenger.swift @@ -4,7 +4,7 @@ import Foundation public protocol Messenger: Sendable { /// Send a message through this messenger /// - Parameter message: The message to send - func send(_ message: S) async throws where S.Element == Character + func send(_ message: Data) async throws /// Close the messenger func close() async throws @@ -15,15 +15,3 @@ public protocol Messenger: Sendable { /// - code: An error code func error(_ message: String, code: Int) async throws } - -extension Messenger { - /// Send a message through this messenger - /// - Parameter message: The message to send - func send(_ message: Data) async throws { - // TODO: Ideally Data is our native interface, and String is the extension. - // Since that change is breaking, we will do it on the next major version. - if let string = String(data: message, encoding: .utf8) { - try await send(string) - } - } -} diff --git a/Sources/GraphQLWS/Server.swift b/Sources/GraphQLWS/Server.swift index 5b297a7..3dc5f7f 100644 --- a/Sources/GraphQLWS/Server.swift +++ b/Sources/GraphQLWS/Server.swift @@ -52,6 +52,10 @@ where self.onOperationComplete = onOperationComplete self.onOperationError = onOperationError } + + deinit { + subscriptionTasks.values.forEach { $0.cancel() } + } /// Listen and react to the provided async sequence of client messages. This function will block until the stream is completed. /// - Parameter incoming: The client message sequence that the server should react to. @@ -127,10 +131,6 @@ where } } - deinit { - subscriptionTasks.values.forEach { $0.cancel() } - } - private func onConnectionInit( _ connectionInitRequest: ConnectionInitRequest, _: Messenger diff --git a/Tests/GraphQLWSTests/Utils/TestMessenger.swift b/Tests/GraphQLWSTests/Utils/TestMessenger.swift index e1ea713..ee24db4 100644 --- a/Tests/GraphQLWSTests/Utils/TestMessenger.swift +++ b/Tests/GraphQLWSTests/Utils/TestMessenger.swift @@ -14,10 +14,8 @@ actor TestMessenger: Messenger { self.continuation = continuation } - func send(_ message: S) async throws where S.Element == Character { - if let data = String(message).data(using: .utf8) { - continuation.yield(data) - } + func send(_ message: Data) async throws { + continuation.yield(message) } func error(_ message: String, code: Int) async throws { From f5830f127a3ca3b3cc624f3dd3949a1ce2f4f8d5 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 12 May 2026 20:45:48 -0600 Subject: [PATCH 4/5] docs: Readme updates --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5915c98..5f4d956 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ To use this package, include it in your `Package.swift` dependencies: .package(url: "https://github.com/GraphQLSwift/GraphQLWS", from: ""), ``` -Then create a class to implement the `Messenger` protocol. Here's an example using +Then create a concrete type that conforms to the `Messenger` protocol. Here's an example using [`WebSocketKit`](https://github.com/vapor/websocket-kit): ```swift @@ -31,12 +31,12 @@ import GraphQLWS struct WebSocketMessenger: Messenger { let websocket: WebSocket - func send(_ message: S) async throws where S: Collection, S.Element == Character async throws { - try await websocket.send(message) + func send(_ message: Data) async throws { + try await websocket.send(String(decoding: message, as: UTF8.self)) } func error(_ message: String, code: Int) async throws { - try await websocket.send("\(code): \(message)") + try await websocket.close(code: code) } func close() async throws { @@ -73,9 +73,9 @@ routes.webSocket( ) } ) - let incoming = AsyncStream { continuation in + let incoming = AsyncStream { continuation in websocket.onText { _, message in - continuation.yield(message) + continuation.yield(Data(message.utf8)) } } try await server.listen(to: incoming) From 5f5f416b91ab4a668902d4ad9b630f9cedb258ad Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 13 May 2026 00:28:03 -0600 Subject: [PATCH 5/5] chore!: change the error code enum to `internal` visibility --- Sources/GraphQLWS/GraphQLWSError.swift | 2 +- Tests/GraphQLWSTests/GraphQLWSTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/GraphQLWS/GraphQLWSError.swift b/Sources/GraphQLWS/GraphQLWSError.swift index 52dec61..f2d11e0 100644 --- a/Sources/GraphQLWS/GraphQLWSError.swift +++ b/Sources/GraphQLWS/GraphQLWSError.swift @@ -88,7 +88,7 @@ struct GraphQLWSError: Error { } /// Error codes for miscellaneous issues -public enum ErrorCode: Int, CustomStringConvertible, Sendable { +enum ErrorCode: Int, CustomStringConvertible, Sendable { /// Miscellaneous case miscellaneous = 4400 diff --git a/Tests/GraphQLWSTests/GraphQLWSTests.swift b/Tests/GraphQLWSTests/GraphQLWSTests.swift index 1fe244b..ad350ad 100644 --- a/Tests/GraphQLWSTests/GraphQLWSTests.swift +++ b/Tests/GraphQLWSTests/GraphQLWSTests.swift @@ -52,7 +52,7 @@ struct GraphqlTransportWSTests { #expect( error == TestMessengerError( - code: ErrorCode.notInitialized.rawValue, + code: 4431, message: "Connection not initialized" ) ) @@ -99,7 +99,7 @@ struct GraphqlTransportWSTests { #expect( error == TestMessengerError( - code: ErrorCode.unauthorized.rawValue, + code: 4430, message: "Unauthorized" ) )