Skip to content

Commit

Permalink
Add graphql-ws protocol support (apollographql#2168)
Browse files Browse the repository at this point in the history
* Implement graphql-transport-ws protocol support
* Add graphql-transport-ws integration test based on Apollo Server docs-examples
* Add CI step for Apollo Server graphql-transport-ws tests
* After installing node v12 switch to use v16
* Instruct nvm to use version in .nvmrc
* Update documentation and tutorial
* Change WSProtocol cases to closer match library names
* Remove initializer defaults and require web socket protocol on designated initializer.
* Update Subscriptions documentation
* Add WSProtocol option for AWS AppSync
* Add ping/pong message support required by graphql-ws
* Update documentation and tutorial
* Add tests for subscriptionWsProtocol
* Add tests for graphqlWSProtocol
* Revert to naming aligned with the protocols and not the implementation libraries
* Use longer async timeout for slower environments like CI
* Fix test names
* Fix project configuration
* Rename protocol parameter on WebSocket initializers
* Revert "Use longer async timeout for slower environments like CI"
* Fix async timing bug and refactor websocket protocol tests
  • Loading branch information
calvincestari authored and jamesonwilliams committed Nov 16, 2024
1 parent 8d6d114 commit 8286ee4
Show file tree
Hide file tree
Showing 29 changed files with 1,041 additions and 62 deletions.
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
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
202 changes: 202 additions & 0 deletions Apollo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

This file was deleted.

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
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
}).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) {}
}
58 changes: 47 additions & 11 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 {
/// 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.
/// - protocol: Protocol to use for communication over the web socket.
public init(request: URLRequest, protocol: WSProtocol) {
self.request = request
self.stream = FoundationStream()
if request.value(forHTTPHeaderField: Constants.headerOriginName) == nil {
Expand All @@ -197,20 +217,36 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock
self.request.setValue(origin, forHTTPHeaderField: Constants.headerOriginName)
}

self.request.setValue(Constants.headerWSProtocolValue,
forHTTPHeaderField: Constants.headerWSProtocolName)
self.request.setValue(`protocol`.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.
/// - protocol: Protocol to use for communication over the web socket.
public convenience init(url: URL, protocol: WSProtocol) {
var request = URLRequest(url: url)
request.timeoutInterval = 5
self.init(request: request)

self.init(request: request, protocol: `protocol`)
}

// 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.
/// - protocol: Protocol to use for communication over the web socket.
public convenience init(
url: URL,
writeQueueQOS: QualityOfService,
protocol: WSProtocol
) {
self.init(url: url, protocol: `protocol`)
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
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
}

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class StarWarsSubscriptionTests: XCTestCase {

webSocketTransport = WebSocketTransport(
websocket: WebSocket(
request: URLRequest(url: TestServerURL.starWarsWebSocket.url)
request: URLRequest(url: TestServerURL.starWarsWebSocket.url),
protocol: .graphql_ws
),
store: ApolloStore()
)
Expand Down
Loading

0 comments on commit 8286ee4

Please sign in to comment.