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
- )
- }
}
}