Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add graphql-ws protocol support #2168

Merged
merged 21 commits into from
Feb 25, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6e7af18
Implement graphql-transport-ws protocol support
calvincestari Feb 18, 2022
d60ae4a
Add graphql-transport-ws integration test based on Apollo Server docs…
calvincestari Feb 18, 2022
685d359
Add CI step for Apollo Server graphql-transport-ws tests
calvincestari Feb 18, 2022
c36b779
After installing node v12 switch to use v16
calvincestari Feb 18, 2022
b1fc644
Instruct nvm to use version in .nvmrc
calvincestari Feb 18, 2022
6768fde
Update documentation and tutorial
calvincestari Feb 18, 2022
e8d1123
Change WSProtocol cases to closer match library names
calvincestari Feb 21, 2022
7f6c4b6
Remove initializer defaults and require web socket protocol on design…
calvincestari Feb 21, 2022
ad9fbb5
Update Subscriptions documentation
calvincestari Feb 21, 2022
37336c5
Add WSProtocol option for AWS AppSync
calvincestari Feb 22, 2022
348dcf0
Add ping/pong message support required by graphql-ws
calvincestari Feb 22, 2022
3391f33
Update documentation and tutorial
calvincestari Feb 23, 2022
5b3aafb
Add tests for subscriptionWsProtocol
calvincestari Feb 24, 2022
6edea9b
Add tests for graphqlWSProtocol
calvincestari Feb 24, 2022
4178c66
Revert to naming aligned with the protocols and not the implementatio…
calvincestari Feb 24, 2022
d63d078
Use longer async timeout for slower environments like CI
calvincestari Feb 24, 2022
fa3f459
Fix test names
calvincestari Feb 24, 2022
118745d
Fix project configuration
calvincestari Feb 25, 2022
3589cad
Rename protocol parameter on WebSocket initializers
calvincestari Feb 25, 2022
2a92350
Revert "Use longer async timeout for slower environments like CI"
calvincestari Feb 25, 2022
9c92529
Fix async timing bug and refactor websocket protocol tests
calvincestari Feb 25, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ commands:
steps:
- restore_cache:
key: starwars-server
- restore_cache:
key: apollo-server-graphql-transport-ws
- common_test_setup
- run:
command: ./scripts/install-node.sh
command: ./scripts/install-node-v12.sh
name: Install Node
- run:
command: ./scripts/install-or-update-starwars-server.sh
Expand All @@ -43,18 +45,29 @@ commands:
name: Start StarWars Server
background: true
- run:
command: cd SimpleUploadServer && npm install && npm start
command: cd SimpleUploadServer && nvm use && npm install && npm start
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required to tell nvm to actually use the .nvmrc file created to lock SimpleUploadServer to node v12.

name: Start Upload Server
background: true
- run:
command: sudo chmod -R +rwx SimpleUploadServer
name: Adjust permissions for simple upload server folder
- run:
command: ./scripts/install-apollo-server-docs-example-server.sh
name: Install Apollo Server (graphql-transport-ws configuration)
- run:
command: cd ../docs-examples/apollo-server/v3/subscriptions-graphql-ws && npm start
name: Start Apollo Server (graphql-transport-ws configuration)
background: true
integration_test_cleanup:
steps:
- save_cache:
key: starwars-server
paths:
- ../starwars-server
- save_cache:
key: apollo-server-graphql-transport-ws
paths:
- ../docs-examples/apollo-server/v3/subscriptions-graphql-ws
common_test_setup:
description: Commands to run for setup of every set of tests
steps:
Expand Down
206 changes: 206 additions & 0 deletions Apollo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
{
"object": {
"pins": [
{
"package": "CwlCatchException",
"repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git",
"state": {
"branch": null,
"revision": "35f9e770f54ce62dd8526470f14c6e137cef3eea",
"version": "2.1.1"
}
},
{
"package": "CwlPreconditionTesting",
"repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git",
"state": {
"branch": null,
"revision": "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688",
"version": "2.1.0"
}
},
{
"package": "Nimble",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding Nimble to main, currently it only exists in the 1.0 branches.

"repositoryURL": "https://github.com/Quick/Nimble",
"state": {
"branch": null,
"revision": "c93f16c25af5770f0d3e6af27c9634640946b068",
"version": "9.2.1"
}
},
{
"package": "SQLite.swift",
"repositoryURL": "https://github.com/stephencelis/SQLite.swift.git",
Expand Down
3 changes: 3 additions & 0 deletions Configuration/Apollo/Apollo-Target-SubscriptionAPI.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#include "../Shared/Workspace-Universal-Framework.xcconfig"

INFOPLIST_FILE = Sources/SubscriptionAPI/Info.plist
1 change: 1 addition & 0 deletions SimpleUploadServer/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v12.22.10
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleUploadServer needs to be upgraded to the latest Apollo Server at some point but for now we have to do this. ☹️

4 changes: 3 additions & 1 deletion SimpleUploadServer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ const server = new ApolloServer({
}
});

server.listen().then(({ url }) => {
server.listen({
port: 4001
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Apollo Server documentation example defaults to port 4000. Since it's maintained by other teams and serves as a documentation example it's easier for us to move the SimpleUploadServer service to port 4001.

}).then(({ url }) => {
console.info(`Upload server started at ${url}`);
});
18 changes: 18 additions & 0 deletions Sources/ApolloTestSupport/MockWebSocketDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation
@testable import ApolloWebSocket

public class MockWebSocketDelegate: WebSocketClientDelegate {
public var didReceiveMessage: ((String) -> Void)?

public init() {}

public func websocketDidConnect(socket: WebSocketClient) {}

public func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {}

public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
didReceiveMessage?(text)
}

public func websocketDidReceiveData(socket: WebSocketClient, data: Data) {}
}
57 changes: 47 additions & 10 deletions Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,30 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock
public let code: Int
}

private struct Constants {
/// The GraphQL over WebSocket protocols supported by apollo-ios.
public enum WSProtocol: CustomStringConvertible {
calvincestari marked this conversation as resolved.
Show resolved Hide resolved
/// WebSocket protocol `graphql-ws`. This is implemented by the [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws)
/// and AWS AppSync libraries.
case graphql_ws
/// WebSocket protocol `graphql-transport-ws`. This is implemented by the [graphql-ws](https://github.com/enisdenjo/graphql-ws)
/// library.
case graphql_transport_ws

public var description: String {
switch self {
case .graphql_ws: return "graphql-ws"
case .graphql_transport_ws: return "graphql-transport-ws"
}
}
}

struct Constants {
static let headerWSUpgradeName = "Upgrade"
static let headerWSUpgradeValue = "websocket"
static let headerWSHostName = "Host"
static let headerWSConnectionName = "Connection"
static let headerWSConnectionValue = "Upgrade"
static let headerWSProtocolName = "Sec-WebSocket-Protocol"
static let headerWSProtocolValue = "graphql-ws"
static let headerWSVersionName = "Sec-WebSocket-Version"
static let headerWSVersionValue = "13"
static let headerWSExtensionName = "Sec-WebSocket-Extensions"
Expand Down Expand Up @@ -183,8 +199,12 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock
return canWork
}

/// Used for setting protocols.
public init(request: URLRequest) {
/// Designated initializer.
///
/// - Parameters:
/// - request: A URL request object that provides request-specific information such as the URL.
/// - webSocketProtocol: Protocol to use for communication over the web socket.
public init(request: URLRequest, webSocketProtocol: WSProtocol) {
calvincestari marked this conversation as resolved.
Show resolved Hide resolved
self.request = request
self.stream = FoundationStream()
if request.value(forHTTPHeaderField: Constants.headerOriginName) == nil {
Expand All @@ -197,20 +217,37 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock
self.request.setValue(origin, forHTTPHeaderField: Constants.headerOriginName)
}

self.request.setValue(Constants.headerWSProtocolValue,
self.request.setValue(webSocketProtocol.description,
forHTTPHeaderField: Constants.headerWSProtocolName)

writeQueue.maxConcurrentOperationCount = 1
}

public convenience init(url: URL) {
/// Convenience initializer to specify the URL and web socket protocol.
///
/// - Parameters:
/// - url: The destination URL to connect to.
/// - webSocketProtocol: Protocol to use for communication over the web socket.
public convenience init(url: URL, webSocketProtocol: WSProtocol) {
var request = URLRequest(url: url)
request.timeoutInterval = 5
self.init(request: request)

self.init(request: request, webSocketProtocol: webSocketProtocol)
}

// Used for specifically setting the QOS for the write queue.
public convenience init(url: URL, writeQueueQOS: QualityOfService) {
self.init(url: url)
/// Convenience initializer to specify the URL and web socket protocol with a specific quality of
/// service on the write queue.
///
/// - Parameters:
/// - url: The destination URL to connect to.
/// - writeQueueQOS: Specifies the quality of service for the write queue.
/// - webSocketProtocol: Protocol to use for communication over the web socket.
public convenience init(
url: URL,
writeQueueQOS: QualityOfService,
webSocketProtocol: WSProtocol
) {
self.init(url: url, webSocketProtocol: webSocketProtocol)
writeQueue.qualityOfService = writeQueueQOS
}

Expand Down
13 changes: 12 additions & 1 deletion Sources/ApolloWebSocket/OperationMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ final class OperationMessage {
enum Types : String {
case connectionInit = "connection_init" // Client -> Server
case connectionTerminate = "connection_terminate" // Client -> Server
case subscribe = "subscribe" // Client -> Server
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Messages that are additive so that we can continue to support both protocols.

case start = "start" // Client -> Server
case stop = "stop" // Client -> Server

Expand All @@ -17,6 +18,10 @@ final class OperationMessage {
case data = "data" // Server -> Client
case error = "error" // Server -> Client
case complete = "complete" // Server -> Client
case next = "next" // Server -> Client

case ping = "ping" // Bidirectional
case pong = "pong" // Bidirectional
}

let serializationFormat = JSONSerializationFormat.self
Expand All @@ -34,7 +39,7 @@ final class OperationMessage {

init(payload: GraphQLMap? = nil,
id: String? = nil,
type: Types = .start) {
type: Types) {
var message: GraphQLMap = [:]
if let payload = payload {
message["payload"] = payload
Expand Down Expand Up @@ -99,6 +104,12 @@ final class OperationMessage {
}
}

extension OperationMessage: CustomDebugStringConvertible {
var debugDescription: String {
rawMessage!
}
}

struct ParseHandler {
let type: String?
let id: String?
Expand Down
19 changes: 17 additions & 2 deletions Sources/ApolloWebSocket/WebSocketTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ public class WebSocketTransport {

switch messageType {
case .data,
.next,
.error:
if let id = parseHandler.id, let responseHandler = subscribers[id] {
if let payload = parseHandler.payload {
Expand Down Expand Up @@ -180,11 +181,19 @@ public class WebSocketTransport {
writeQueue()

case .connectionKeepAlive,
.startAck:
.startAck,
.pong:
writeQueue()

case .ping:
if let str = OperationMessage(type: .pong).rawMessage {
write(str)
writeQueue()
}

case .connectionInit,
.connectionTerminate,
.subscribe,
.start,
.stop,
.connectionError:
Expand Down Expand Up @@ -270,7 +279,13 @@ public class WebSocketTransport {
sendQueryDocument: true,
autoPersistQuery: false)
let identifier = operationMessageIdCreator.requestId()
guard let message = OperationMessage(payload: body, id: identifier).rawMessage else {

var type: OperationMessage.Types = .start
if case WebSocket.WSProtocol.graphql_transport_ws.description = websocket.request.value(forHTTPHeaderField: WebSocket.Constants.headerWSProtocolName) {
type = .subscribe
}

guard let message = OperationMessage(payload: body, id: identifier, type: type).rawMessage else {
return nil
}

Expand Down
51 changes: 51 additions & 0 deletions Sources/SubscriptionAPI/API.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// @generated
// This file was automatically generated and should not be edited.

import Apollo
import Foundation

public final class IncrementingSubscription: GraphQLSubscription {
/// The raw GraphQL definition of this operation.
public let operationDefinition: String =
"""
subscription Incrementing {
numberIncremented
}
"""

public let operationName: String = "Incrementing"

public let operationIdentifier: String? = "fe12b5f0dfc7fefa513cc8aecef043b45daf2d776fd000d3a7703f9798ecf233"

public init() {
}

public struct Data: GraphQLSelectionSet {
public static let possibleTypes: [String] = ["Subscription"]

public static var selections: [GraphQLSelection] {
return [
GraphQLField("numberIncremented", type: .scalar(Int.self)),
]
}

public private(set) var resultMap: ResultMap

public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}

public init(numberIncremented: Int? = nil) {
self.init(unsafeResultMap: ["__typename": "Subscription", "numberIncremented": numberIncremented])
}

public var numberIncremented: Int? {
get {
return resultMap["numberIncremented"] as? Int
}
set {
resultMap.updateValue(newValue, forKey: "numberIncremented")
}
}
}
}
24 changes: 24 additions & 0 deletions Sources/SubscriptionAPI/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
11 changes: 11 additions & 0 deletions Sources/SubscriptionAPI/SubscriptionAPI.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>

//! Project version number for SubscriptionAPI.
FOUNDATION_EXPORT double SubscriptionAPIVersionNumber;

//! Project version string for SubscriptionAPI.
FOUNDATION_EXPORT const unsigned char SubscriptionAPIVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <SubscriptionAPI/PublicHeader.h>


6 changes: 6 additions & 0 deletions Sources/SubscriptionAPI/graphql/operation_ids.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"fe12b5f0dfc7fefa513cc8aecef043b45daf2d776fd000d3a7703f9798ecf233": {
"name": "Incrementing",
"source": "subscription Incrementing {\n numberIncremented\n}"
}
}
7 changes: 7 additions & 0 deletions Sources/SubscriptionAPI/graphql/schema.graphqls
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type Query {
currentNumber: Int
}

type Subscription {
numberIncremented: Int
}
4 changes: 4 additions & 0 deletions Sources/SubscriptionAPI/graphql/subscription.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
subscription Incrementing {
numberIncremented
}

Loading