diff --git a/Documentation/pages/docs/features/graphql-over-http.mdx b/Documentation/pages/docs/features/graphql-over-http.mdx index a6e2272..55ef8a0 100644 --- a/Documentation/pages/docs/features/graphql-over-http.mdx +++ b/Documentation/pages/docs/features/graphql-over-http.mdx @@ -82,3 +82,14 @@ It should have no impact on legitimate use of your graph except in these two cas - You implemented and have enabled file uploads through your GraphQL server using `multipart/form-data`. If either of these apply to you and you want to keep the prevention mechanic, you should configure the relevant clients to send a non-empty `Apollo-Require-Preflight` header along with all requests. + + +## GraphQL over HTTP spec compliance + +As of Pioneer v1, Pioneer is spec compliant with the [GraphQL over HTTP spec](https://github.com/graphql/graphql-http#servers). + +### [Details on compliance](https://github.com/graphql/graphql-http/blob/main/implementations/pioneer/README.md) + +- **78** audits in total +- ✅ **75** pass +- ⚠️ **3** warnings (optional) \ No newline at end of file diff --git a/Documentation/pages/docs/features/graphql-over-websocket.mdx b/Documentation/pages/docs/features/graphql-over-websocket.mdx index 42a75c8..453e6b7 100644 --- a/Documentation/pages/docs/features/graphql-over-websocket.mdx +++ b/Documentation/pages/docs/features/graphql-over-websocket.mdx @@ -12,7 +12,7 @@ The newer sub-protocol is [graphql-ws](https://github.com/enisdenjo/graphql-ws). #### Usage -You can to use this sub-protocol by specifying when initializing Pioneer. +You can to use this sub-protocol by specifying when initializing Pioneer. This is the default option. ```swift {3} showLineNumbers copy let server = Pioneer( @@ -25,20 +25,23 @@ let server = Pioneer( Even though the sub-protocol is the recommended and default option, there are still some consideration to take account of. Adoption for this sub-protocol are somewhat limited outside the Node.js / Javascript ecosystem or major GraphQL client libraries. -A good amount of other server implementations on many languages have also yet to support this sub-protocol. So, make sure that libraries and frameworks you are using already have support for [graphql-ws](https://github.com/enisdenjo/graphql-ws). If in doubt, it's best to understand how both sub-protocols work and have options to swap between both options. +A good amount of other server implementations on many languages have also yet to support this sub-protocol. So, make sure that libraries and frameworks you are using already have support for [graphql-ws](https://github.com/enisdenjo/graphql-ws). ### `subscriptions-transport-ws` The older standard is [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws). This is a sub-protocol from the team at Apollo GraphQL, that was created along side [apollo-server](https://github.com/apollographql/apollo-server) and [apollo-client](https://github.com/apollographql/apollo-client). Some clients and servers still use this to perform operations through websocket especially subscriptions. -In the GraphQL ecosystem, subscriptions-transport-ws is considered a legacy protocol. +In the GraphQL ecosystem, subscriptions-transport-ws is considered a legacy protocol and has been archived. + +Pioneer now considers this protcol as legacy, marked as deprecated, and will likely be removed in the future major releases. + More explaination [here](#consideration). #### Usage -By default, Pioneer will already use this sub-protocol to perform GraphQL operations through websocket. +You can to use this sub-protocol by specifying when initializing Pioneer. ```swift {3} showLineNumbers copy let server = Pioneer( diff --git a/Documentation/pages/docs/v1/migrating.mdx b/Documentation/pages/docs/v1/migrating.mdx index aa71cfe..9a967cd 100644 --- a/Documentation/pages/docs/v1/migrating.mdx +++ b/Documentation/pages/docs/v1/migrating.mdx @@ -111,7 +111,7 @@ app.middleware.use( server.vaporMiddleware( context: { req, res in ... - }, + }, websocketContext: { req, payload, gql in ... }, @@ -166,13 +166,18 @@ Pioneer will now defaults to - [.sandbox](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/pioneer/ide/sandbox) for its [WebSocket Protocol](/docs/features/graphql-over-websocket/#websocket-subprotocol) - `30` seconds for the keep alive interval for GraphQL over WebSocket +### Deprecating `subscriptions-transport-ws` + +As of Mar 4 2022, the [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) has been made read-only archive and will be marked as deprecated in Pioneer. +Pioneer will now defaults to the [`graphql-ws`](/docs/features/graphql-over-websocket/#websocket-subprotocol) instead. + ### WebSocket callbacks Some WebSocket callbacks are now exposed as functions in Pioneer. These can be used to add a custom WebSocket layer. - [.receiveMessage](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/pioneer) - Callback to be called for each WebSocket message -- [.initialiseClient](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/pioneer) +- [.createClient](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/pioneer) - Callback after getting a GraphQL over WebSocket initialisation message according to the given protocol - [.executeLongOperation](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/pioneer) - Callback to run long running operation using Pioneer diff --git a/Documentation/pages/docs/web-frameworks/integration.mdx b/Documentation/pages/docs/web-frameworks/integration.mdx index 5370375..edfb862 100644 --- a/Documentation/pages/docs/web-frameworks/integration.mdx +++ b/Documentation/pages/docs/web-frameworks/integration.mdx @@ -34,16 +34,10 @@ struct HTTPGraphQLRequest { } ``` -The important part is parsing into [GraphQLRequest](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/graphqlrequest). A recommended approach in parsing is: +The important part is parsing into [GraphQLRequest](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/graphqlrequest). +This can be done by making sure the web-framework request object conforms to the [GraphQLRequestConvertible](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/graphqlrequestconvertible) protocol. -1. Parse [GraphQLRequest](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/graphqlrequest) from the body of a request. (Usually for **POST**) -2. If it's not in the body, get the values from the query/search parameters. (Usually for **GET**) - - The query string should be under `query` - - The operation name should be under `operationName` - - The variables should be under `variables` as JSON string
- (_This is probably percent encoded, and also need to be parse into `[String: Map]?` if available_) - - As long the query string is accessible, the request is not malformed and we can construct a [GraphQLRequest](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/graphqlrequest) using that. -3. If [GraphQLRequest](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/graphqlrequest) can't be retreive by both approach 1 and 2, the request is malformed and the response could also have status code of 400 Bad Request. +After that, the [GraphQLRequest](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/graphqlrequest) can be accessed from the property [.graphql](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/graphqlrequestconvertible).
Example @@ -51,36 +45,19 @@ The important part is parsing into [GraphQLRequest](https://swiftpackageindex.co ```swift showLineNumbers copy import class WebFramework.Request -extension Request { - var graphql: HTTPGraphQLRequest? { - switch (method) { - // Parsing from body for POST - case .post: - guard let gql = try? JSONDecoder().decode(GraphQLRequest.self, from: self.body) else { - return nil - } - return .init(request: gql, headers: headers, method: method) +extension Request: GraphQLRequestConvertible { + public func body(_ decodable: T.Type) throws -> T where T: Decodable { + try JSONDecoder().decode(decodable, from: body) + } - // Parsing from query/search params for GET - case .get: - guard let query = self.search["query"] else { - return nil - } - let operationName = self.search["operationName"] - let variables = self.search["variables"]? - .removingPercentEncoding - .flatMap { - $0.data(using: .utf8) - } - .flatMap { - try? JSONDecoder().decode([String: Map].self, from: $0) - } - let gql = GraphQLRequest(query: query, operationName: operationName, variables: variables) - return .init(request: gql, headers: headers, method: method) - - default: - return nil - } + public func searchParams(_ decodable: T.Type, at: String) -> T? where T: Decodable { + search[at]?.removingPercentEncoding + .flatMap { $0.data(using: .utf8) } + .flatMap { try? JSONDecoder().decode(decodable, from: $0) } + } + + public var isAcceptingGraphQLResponse: Bool { + headers[.accept].contains(HTTPGraphQLRequest.mediaType) } } ``` @@ -119,10 +96,15 @@ struct HTTPGraphQLResponse { } ``` + +The property [.graphql](#mapping-into-httpgraphqlrequest) may throw a [GraphQLViolation](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/graphqlviolation) error. +This error should be caught, the its message and status value should be use in the response to comply with the GraphQL over HTTP specification. + +
Example -```swift {9-14,16-19,23-25} showLineNumbers copy +```swift {9-14,16-19,23-24,26-28} showLineNumbers copy import class WebFramework.Request import class WebFramework.Response import struct Pioneer.Pioneer @@ -144,6 +126,9 @@ extension Pioneer { res.status = httpRes.status return res + } catch let e as GraphQLViolation { + let body = try GraphQLJSONEncoder().encode(GraphQLResult(data: nil, errors: [.init(e.message)])) + return Response(status: e.status(req.isAcceptingGraphQLResponse), body: body) } catch { // Format error caught into GraphQLResult let body = try GraphQLJSONEncoder().encode(GraphQLResult(data: nil, errors: [.init(error)])) @@ -288,7 +273,7 @@ After the upgrade is done, there's only a few things to do: - [.receiveMessage](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/pioneer) method is used here. - For consuming the incoming message, if in the web-framework it is done in a callback, it is best to pipe that value into an AsyncStream first and iterate through the AsyncStream before calling the [.receiveMessage](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/pioneer) method. - Setting up callback for when the connection has been closed. - - [.closeClient](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/pioneer) method is used here. + - [.disposeClient](https://swiftpackageindex.com/d-exclaimation/pioneer/documentation/pioneer/pioneer) method is used here. - It is also recommended if possible to stop the consuming incoming message here as well.
@@ -356,7 +341,7 @@ extension Pioneer { Task { try await ws.onClose.get() receiving.cancel() - closeClient(cid: cid, keepAlive: keepAlive, timeout: timeout) + disposeClient(cid: cid, keepAlive: keepAlive, timeout: timeout) } } } diff --git a/LICENSE b/LICENSE index c969257..c68e3e3 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 d-exclaimation + Copyright 2023 d-exclaimation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Sources/Pioneer/GraphQL/GraphQLRequest.swift b/Sources/Pioneer/GraphQL/GraphQLRequest.swift index cdb80c8..2a05d3f 100644 --- a/Sources/Pioneer/GraphQL/GraphQLRequest.swift +++ b/Sources/Pioneer/GraphQL/GraphQLRequest.swift @@ -5,8 +5,8 @@ // Created by d-exclaimation on 12:49 AM. // -import Foundation import GraphQL +import enum NIOHTTP1.HTTPResponseStatus /// GraphQL Request according to the spec public struct GraphQLRequest: Codable, @unchecked Sendable { @@ -34,7 +34,7 @@ public struct GraphQLRequest: Codable, @unchecked Sendable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: Key.self) guard container.contains(.query) else { - throw ParsingIssue.missingQuery + throw GraphQLViolation.missingQuery } do { let query = try container.decode(String.self, forKey: .query) @@ -48,7 +48,7 @@ public struct GraphQLRequest: Codable, @unchecked Sendable { extensions: extensions ?? nil ) } catch { - throw ParsingIssue.invalidForm + throw GraphQLViolation.invalidForm } } @@ -103,10 +103,4 @@ public struct GraphQLRequest: Codable, @unchecked Sendable { } } } - - /// Known possible failure in parsing GraphQLRequest - public enum ParsingIssue: Error, Sendable { - case missingQuery - case invalidForm - } } diff --git a/Sources/Pioneer/GraphQL/GraphQLViolation.swift b/Sources/Pioneer/GraphQL/GraphQLViolation.swift new file mode 100644 index 0000000..d98e532 --- /dev/null +++ b/Sources/Pioneer/GraphQL/GraphQLViolation.swift @@ -0,0 +1,68 @@ +// +// GraphQLViolation.swift +// pioneer +// +// Created by d-exclaimation on 20:04. +// + +import enum NIOHTTP1.HTTPResponseStatus + +/// Violation to the GraphQL over HTTP spec +public struct GraphQLViolation: Error, Sendable, Equatable { + /// Different HTTP status codes for different media type as per GraphQL over HTTP spec + public struct ResponseStatuses: Sendable, Equatable { + /// Status for application/json + public var json: HTTPResponseStatus + /// Status for application/graphql-response+json + public var graphql: HTTPResponseStatus + + public init(json: HTTPResponseStatus, graphql: HTTPResponseStatus) { + self.json = json + self.graphql = graphql + } + } + + /// Default message for this error + public var message: String + /// Appopriate HTTP status code for this error as per GraphQL over HTTP spec + public var status: ResponseStatuses + + public init(message: String, status: HTTPResponseStatus) { + self.message = message + self.status = .init(json: status, graphql: status) + } + + public init(message: String, status: ResponseStatuses) { + self.message = message + self.status = status + } + + /// Get the appropriate HTTP status code for the media type + /// - Parameter isAcceptingGraphQLResponse: If the accept media type is application/graphql-response+json + /// - Returns: HTTP status code + public func status(_ isAcceptingGraphQLResponse: Bool) -> HTTPResponseStatus { + isAcceptingGraphQLResponse ? status.graphql : status.json + } + + static var missingQuery: Self { + .init( + message: "Missing query in request", + status: .init(json: .ok, graphql: .badRequest) + ) + } + + static var invalidForm: Self { + .init( + message: "Invalid GraphQL request form", + status: .init(json: .ok, graphql: .badRequest) + ) + } + + static var invalidMethod: Self { + .init(message: "Invalid HTTP method for a GraphQL request", status: .badRequest) + } + + static var invalidContentType: Self { + .init(message: "Invalid or missing content-type", status: .badRequest) + } +} diff --git a/Sources/Pioneer/Http/HTTPGraphQL.swift b/Sources/Pioneer/Http/HTTPGraphQL.swift index f2468cb..e10f4a3 100644 --- a/Sources/Pioneer/Http/HTTPGraphQL.swift +++ b/Sources/Pioneer/Http/HTTPGraphQL.swift @@ -75,12 +75,6 @@ public struct HTTPGraphQLRequest: Sendable { /// GraphQL over HTTP spec's content type public static var contentType = "\(mediaType); charset=utf-8, \(mediaType)" - - /// Known possible failure in converting HTTP into GraphQL over HTTP request - public enum Issue: Error, Sendable { - case invalidMethod - case invalidContentType - } } /// A type that can be transformed into GraphQLRequest and HTTPGraphQLRequest @@ -101,7 +95,7 @@ public protocol GraphQLRequestConvertible { /// - decodable: Decodable type /// - at: Name of field to decode /// - Returns: The parsed payload if possible, otherwise nil - func urlQuery(_ decodable: T.Type, at: String) -> T? + func searchParams(_ decodable: T.Type, at: String) -> T? } public extension GraphQLRequestConvertible { @@ -110,20 +104,20 @@ public extension GraphQLRequestConvertible { get throws { switch method { case .GET: - guard let query = urlQuery(String.self, at: "query") else { - throw GraphQLRequest.ParsingIssue.missingQuery + guard let query = searchParams(String.self, at: "query") else { + throw GraphQLViolation.missingQuery } - let variables: [String: Map]? = self.urlQuery(String.self, at: "variables") + let variables: [String: Map]? = self.searchParams(String.self, at: "variables") .flatMap { $0.data(using: .utf8)?.to([String: Map].self) } - let operationName: String? = self.urlQuery(String.self, at: "operationName") + let operationName: String? = self.searchParams(String.self, at: "operationName") return GraphQLRequest(query: query, operationName: operationName, variables: variables) case .POST: guard !headers[.contentType].isEmpty else { - throw HTTPGraphQLRequest.Issue.invalidContentType + throw GraphQLViolation.invalidContentType } return try body(GraphQLRequest.self) default: - throw HTTPGraphQLRequest.Issue.invalidMethod + throw GraphQLViolation.invalidMethod } } } diff --git a/Sources/Pioneer/Pioneer.swift b/Sources/Pioneer/Pioneer.swift index 3b66dff..ffce66c 100644 --- a/Sources/Pioneer/Pioneer.swift +++ b/Sources/Pioneer/Pioneer.swift @@ -69,22 +69,18 @@ public struct Pioneer { self.validationRules = validationRules self.keepAlive = keepAlive self.timeout = timeout - - let proto: SubProtocol.Type = expression { - switch websocketProtocol { - case .graphqlWs: - return GraphQLWs.self - default: - return SubscriptionTransportWs.self - } - } - - let probe = Probe( + self.probe = .init( schema: schema, resolver: resolver, - proto: proto + proto: expression { + switch websocketProtocol { + case .subscriptionsTransportWs: + return SubscriptionTransportWs.self + default: + return GraphQLWs.self + } + } ) - self.probe = probe } /// Guard for operation allowed @@ -192,7 +188,7 @@ public struct Pioneer { case let .initial(payload): do { try await check(payload) - await initialiseClient( + await createClient( cid: cid, io: io, payload: payload, @@ -265,18 +261,20 @@ public struct Pioneer { /// - timeout: The timeout interval for the client /// - ev: Any event loop /// - context: The context builder for the client - public func initialiseClient( - cid: UUID, + @discardableResult + public func createClient( + cid: WebSocketClient.ID, io: WebSocketable, payload: Payload, timeout: Task?, ev: EventLoopGroup, context: @escaping WebSocketContext - ) async { + ) async -> WebSocketClient { let client = WebSocketClient(id: cid, io: io, payload: payload, ev: ev, context: context) await probe.connect(with: client) websocketProtocol.initialize(io) timeout?.cancel() + return client } /// Close a client connected through Pioneer.Probe @@ -284,7 +282,7 @@ public struct Pioneer { /// - cid: The client key /// - keepAlive: The client's keepAlive interval /// - timeout: The client's timeout interval - public func closeClient(cid: UUID, keepAlive: Task?, timeout: Task?) { + public func disposeClient(cid: WebSocketClient.ID, keepAlive: Task?, timeout: Task?) { Task { await probe.disconnect(for: cid) } @@ -292,13 +290,13 @@ public struct Pioneer { timeout?.cancel() } - /// Execute long-lived operation through Pioneer.Probe for a GraphQLRequest, context and get a well formatted GraphQlResult + /// Execute subscription through Pioneer.Probe for a GraphQLRequest, context and get a well formatted GraphQlResult /// - Parameters: /// - cid: The client key /// - io: The client IO for outputting errors /// - oid: The key for this operation /// - gql: The GraphQL Request for this operation - public func executeLongOperation(cid: UUID, io: WebSocketable, oid: String, gql: GraphQLRequest) async { + public func executeLongOperation(cid: WebSocketClient.ID, io: WebSocketable, oid: String, gql: GraphQLRequest) async { // Introspection guard guard allowed(from: gql) else { let err = GraphQLMessage.errors(id: oid, type: websocketProtocol.error, [ @@ -325,7 +323,7 @@ public struct Pioneer { /// - io: The client IO for outputting errors /// - oid: The key for this operation /// - gql: The GraphQL Request for this operation - public func executeShortOperation(cid: UUID, io: WebSocketable, oid: String, gql: GraphQLRequest) async { + public func executeShortOperation(cid: WebSocketClient.ID, io: WebSocketable, oid: String, gql: GraphQLRequest) async { // Introspection guard guard allowed(from: gql) else { let err = GraphQLMessage.errors(id: oid, type: websocketProtocol.error, [ @@ -339,6 +337,7 @@ public struct Pioneer { return io.out(err.jsonString) } + // Execute operation at actor level to not block or exhaust the event loop await probe.once( for: cid, with: oid, diff --git a/Sources/Pioneer/Vapor/Extensions/Request/Request+GraphQLRequest.swift b/Sources/Pioneer/Vapor/Extensions/Request/Request+GraphQLRequest.swift index 9548399..3500402 100644 --- a/Sources/Pioneer/Vapor/Extensions/Request/Request+GraphQLRequest.swift +++ b/Sources/Pioneer/Vapor/Extensions/Request/Request+GraphQLRequest.swift @@ -5,9 +5,6 @@ // Created by d-exclaimation on 12:30. // -import struct GraphQL.GraphQLError -import enum GraphQL.Map -import struct Vapor.Abort import class Vapor.Request extension Request: GraphQLRequestConvertible { @@ -15,7 +12,7 @@ extension Request: GraphQLRequestConvertible { try content.decode(decodable) } - public func urlQuery(_ decodable: T.Type, at: String) -> T? where T: Decodable { + public func searchParams(_ decodable: T.Type, at: String) -> T? where T: Decodable { query[decodable, at: at] } diff --git a/Sources/Pioneer/Vapor/Http/Pioneer+Http.swift b/Sources/Pioneer/Vapor/Http/Pioneer+Http.swift index 55477e0..6ec619e 100644 --- a/Sources/Pioneer/Vapor/Http/Pioneer+Http.swift +++ b/Sources/Pioneer/Vapor/Http/Pioneer+Http.swift @@ -42,18 +42,9 @@ public extension Pioneer { res.headers.replaceOrAdd(name: $0, value: $1) } return res - } catch GraphQLRequest.ParsingIssue.missingQuery { - return try GraphQLError(message: "Missing query parameter") - .response(with: req.isAcceptingGraphQLResponse ? .badRequest : .ok) - } catch GraphQLRequest.ParsingIssue.invalidForm { - return try GraphQLError(message: "nvalid GraphQL request form") - .response(with: req.isAcceptingGraphQLResponse ? .badRequest : .ok) - } catch HTTPGraphQLRequest.Issue.invalidMethod { - return try GraphQLError(message: "Invalid HTTP method for a GraphQL request") - .response(with: .badRequest) - } catch HTTPGraphQLRequest.Issue.invalidContentType { - return try GraphQLError(message: "Invalid or missing content-type") - .response(with: .badRequest) + } catch let v as GraphQLViolation { + return try GraphQLError(message: v.message) + .response(with: v.status(req.isAcceptingGraphQLResponse)) } catch let error as AbortError { return try error.response(using: res) } catch { diff --git a/Sources/Pioneer/Vapor/WebSocket/Pioneer+WebSocket.swift b/Sources/Pioneer/Vapor/WebSocket/Pioneer+WebSocket.swift index 6765bb1..11c85fc 100644 --- a/Sources/Pioneer/Vapor/WebSocket/Pioneer+WebSocket.swift +++ b/Sources/Pioneer/Vapor/WebSocket/Pioneer+WebSocket.swift @@ -78,7 +78,7 @@ public extension Pioneer { Task { try await ws.onClose.get() receiving.cancel() - closeClient(cid: cid, keepAlive: keepAlive, timeout: timeout) + disposeClient(cid: cid, keepAlive: keepAlive, timeout: timeout) } } } diff --git a/Sources/Pioneer/WebSocket/Common/WebSocketClient.swift b/Sources/Pioneer/WebSocket/Common/WebSocketClient.swift index feab0f7..53ead76 100644 --- a/Sources/Pioneer/WebSocket/Common/WebSocketClient.swift +++ b/Sources/Pioneer/WebSocket/Common/WebSocketClient.swift @@ -18,21 +18,21 @@ public extension Pioneer { typealias WebSocketContext = @Sendable (Payload, GraphQLRequest) async throws -> Context /// Full GraphQL over WebSocket Client - struct WebSocketClient { + struct WebSocketClient: Identifiable { /// The unique key for this client - var id: UUID + public var id: UUID /// The WebSocket output - var io: WebSocketable + public var io: WebSocketable /// The payload given during initialisation - var payload: Payload + public var payload: Payload /// Any event loop - var ev: EventLoopGroup + public var ev: EventLoopGroup /// Context builder for this client - var contextBuilder: WebSocketContext + public var contextBuilder: WebSocketContext /// Create a GraphQL over WebSocket client /// - Parameters: @@ -41,7 +41,7 @@ public extension Pioneer { /// - payload: The payload given during initialisation /// - ev: Any event loop /// - context: Context builder for this client - init(id: UUID, io: WebSocketable, payload: Payload, ev: EventLoopGroup, context: @escaping WebSocketContext) { + public init(id: UUID, io: WebSocketable, payload: Payload, ev: EventLoopGroup, context: @escaping WebSocketContext) { self.id = id self.io = io self.payload = payload diff --git a/Sources/Pioneer/WebSocket/Probe/Probe.swift b/Sources/Pioneer/WebSocket/Probe/Probe.swift index 6362e52..732e169 100644 --- a/Sources/Pioneer/WebSocket/Probe/Probe.swift +++ b/Sources/Pioneer/WebSocket/Probe/Probe.swift @@ -18,25 +18,19 @@ extension Pioneer { private let proto: SubProtocol.Type init( - schema: GraphQLSchema, resolver: Resolver, proto: SubProtocol.Type + schema: GraphQLSchema, + resolver: Resolver, + proto: SubProtocol.Type ) { self.schema = schema self.resolver = resolver self.proto = proto } - init( - schema: Schema, resolver: Resolver, proto: SubProtocol.Type - ) { - self.schema = schema.schema - self.resolver = resolver - self.proto = proto - } - // MARK: - Private mutable states - private var clients: [UUID: WebSocketClient] = [:] - private var drones: [UUID: Drone] = [:] + private var clients: [WebSocketClient.ID: WebSocketClient] = [:] + private var drones: [WebSocketClient.ID: Drone] = [:] // MARK: - Event callbacks @@ -46,14 +40,14 @@ extension Pioneer { } /// Deallocate the space from a closing process - func disconnect(for cid: UUID) async { + func disconnect(for cid: WebSocketClient.ID) async { await drones[cid]?.acid() clients.delete(cid) drones.delete(cid) } /// Long running operation require its own actor, thus initialing one if there were none prior - func start(for cid: UUID, with oid: String, given gql: GraphQLRequest) async { + func start(for cid: WebSocketClient.ID, with oid: String, given gql: GraphQLRequest) async { guard let client = clients[cid] else { return } @@ -69,7 +63,7 @@ extension Pioneer { } /// Short lived operation is processed immediately and pipe back later - func once(for cid: UUID, with oid: String, given gql: GraphQLRequest) async { + func once(for cid: WebSocketClient.ID, with oid: String, given gql: GraphQLRequest) async { guard let client = clients[cid] else { return } @@ -96,7 +90,7 @@ extension Pioneer { } /// Stopping any operation to client specific actor - func stop(for cid: UUID, with oid: String) async { + func stop(for cid: WebSocketClient.ID, with oid: String) async { await drones[cid]?.stop(for: oid) } @@ -106,27 +100,20 @@ extension Pioneer { client.out(GraphQLMessage(id: oid, type: proto.complete).jsonString) } - // MARK: - Utility methods - /// Build context and execute short-lived GraphQL Operation inside an event loop private func execute(_ gql: GraphQLRequest, client: WebSocketClient) -> Task { Task { [unowned self] in let ctx = try await client.context(gql) - return try await self.executeOperation(for: gql, with: ctx, using: client.ev) + return try await executeGraphQL( + schema: self.schema, + request: gql.query, + resolver: self.resolver, + context: ctx, + eventLoopGroup: client.ev, + variables: gql.variables, + operationName: gql.operationName + ) } } - - /// Execute short-lived GraphQL Operation - private func executeOperation(for gql: GraphQLRequest, with ctx: Context, using eventLoop: EventLoopGroup) async throws -> GraphQLResult { - try await executeGraphQL( - schema: self.schema, - request: gql.query, - resolver: self.resolver, - context: ctx, - eventLoopGroup: eventLoop, - variables: gql.variables, - operationName: gql.operationName - ) - } } }