From c57099e16eff1fa376f4b5683ef2b4429f1ca85e Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 30 Jul 2020 15:06:37 -0500 Subject: [PATCH 1/7] Add pause/resume functionality to web socket transport --- .../ApolloWebSocket/WebSocketTransport.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 74101f2e68..9f8b98adce 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -323,6 +323,23 @@ public class WebSocketTransport { reconnect.value = oldReconnectValue } + + /// Disconnects the websocket while setting the auto-reconnect value to false, + /// allowing purposeful disconnects that do not dump existing subscriptions. + /// NOTE: You will receive an error on the subscription (should be a `Starscream.WSError` with code 1000) when the socket disconnects. + /// ALSO NOTE: To reconnect after calling this, you will need to call `resumeWebSocketConnection`. + public func pauseWebSocketConnection() { + self.reconnect.value = false + self.websocket.disconnect() + } + + /// Reconnects a paused web socket. + /// + /// - Parameter autoReconnect: `true` if you want the websocket to automatically reconnect if the connection drops. Defaults to true. + public func resumeWebSocketConnection(autoReconnect: Bool = true) { + self.reconnect.value = autoReconnect + self.websocket.connect() + } } // MARK: - HTTPNetworkTransport conformance From bb6771609c9c1044ef8626bb7747c7441d6b3ad1 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 30 Jul 2020 15:06:57 -0500 Subject: [PATCH 2/7] Add test of pause/resume functionality for websocket transport --- .../StarWarsSubscriptionTests.swift | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index 7cb62c67cd..17b41c4f36 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -3,6 +3,7 @@ import Apollo import ApolloTestSupport @testable import ApolloWebSocket import StarWarsAPI +import Starscream class StarWarsSubscriptionTests: XCTestCase { let SERVER: String = "ws://localhost:8080/websocket" @@ -12,6 +13,8 @@ class StarWarsSubscriptionTests: XCTestCase { var webSocketTransport: WebSocketTransport! var connectionStartedExpectation: XCTestExpectation? + var disconnectedExpectation: XCTestExpectation? + var reconnectedExpectation: XCTestExpectation? override func setUp() { super.setUp() @@ -408,6 +411,68 @@ class StarWarsSubscriptionTests: XCTestCase { waitForExpectations(timeout: 10, handler: nil) } + + func testPausingAndResumingWebSocketConnection() { + let subscription = ReviewAddedSubscription() + let reviewMutation = CreateAwesomeReviewMutation() + + // Send the mutations via a separate transport so they can still be sent when the websocket is disconnected + let httpTransport = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) + let httpClient = ApolloClient(networkTransport: httpTransport) + + func sendReview() { + let reviewSentExpectation = self.expectation(description: "review sent") + httpClient.perform(mutation: reviewMutation) { mutationResult in + switch mutationResult { + case .success: + break + case .failure(let error): + XCTFail("Unexpected error sending review: \(error)") + } + + reviewSentExpectation.fulfill() + } + self.wait(for: [reviewSentExpectation], timeout: 10) + } + + let subscriptionExpectation = self.expectation(description: "Received review") + // This should get hit twice - once before we pause the web socket and once after. + subscriptionExpectation.expectedFulfillmentCount = 2 + self.client.subscribe(subscription: subscription) { subscriptionResult in + switch subscriptionResult { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.reviewAdded?.episode, .jedi) + subscriptionExpectation.fulfill() + case .failure(let error): + if let wsError = error as? Starscream.WSError { + // This is an expected error on disconnection, ignore it. + XCTAssertEqual(wsError.code, 1000) + } else { + XCTFail("Unexpected error receiving subscription: \(error)") + subscriptionExpectation.fulfill() + } + } + } + + self.waitForSubscriptionsToStart() + sendReview() + + self.disconnectedExpectation = self.expectation(description: "Web socket disconnected") + webSocketTransport.pauseWebSocketConnection() + self.wait(for: [self.disconnectedExpectation!], timeout: 10) + + // This should not go through since the socket is paused + sendReview() + + self.reconnectedExpectation = self.expectation(description: "Web socket reconnected") + webSocketTransport.resumeWebSocketConnection() + self.wait(for: [self.reconnectedExpectation!], timeout: 10) + + // Now that we've reconnected, this should go through to the same subscription. + sendReview() + + self.wait(for: [subscriptionExpectation], timeout: 10) + } } extension StarWarsSubscriptionTests: WebSocketTransportDelegate { @@ -415,4 +480,12 @@ extension StarWarsSubscriptionTests: WebSocketTransportDelegate { func webSocketTransportDidConnect(_ webSocketTransport: WebSocketTransport) { self.connectionStartedExpectation?.fulfill() } + + func webSocketTransportDidReconnect(_ webSocketTransport: WebSocketTransport) { + self.reconnectedExpectation?.fulfill() + } + + func webSocketTransport(_ webSocketTransport: WebSocketTransport, didDisconnectWithError error: Error?) { + self.disconnectedExpectation?.fulfill() + } } From d4fee175271c6de69a776568ce245dfe831eba53 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 30 Jul 2020 16:11:45 -0500 Subject: [PATCH 3/7] =?UTF-8?q?make=20sure=20subscription=20is=20cancelled?= =?UTF-8?q?=20at=20the=20end=20of=20the=20test=20=F0=9F=A4=A6=E2=80=8D?= =?UTF-8?q?=E2=99=80=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index 17b41c4f36..ca25383010 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -438,7 +438,7 @@ class StarWarsSubscriptionTests: XCTestCase { let subscriptionExpectation = self.expectation(description: "Received review") // This should get hit twice - once before we pause the web socket and once after. subscriptionExpectation.expectedFulfillmentCount = 2 - self.client.subscribe(subscription: subscription) { subscriptionResult in + let sub = self.client.subscribe(subscription: subscription) { subscriptionResult in switch subscriptionResult { case .success(let graphQLResult): XCTAssertEqual(graphQLResult.data?.reviewAdded?.episode, .jedi) @@ -471,6 +471,9 @@ class StarWarsSubscriptionTests: XCTestCase { // Now that we've reconnected, this should go through to the same subscription. sendReview() + // Cancel subscription so it doesn't keep receiving from other tests. + sub.cancel() + self.wait(for: [subscriptionExpectation], timeout: 10) } } From 4eb1f2a6a1026ddd9728dfa2a13a71908bcaf44f Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Fri, 31 Jul 2020 13:16:47 -0500 Subject: [PATCH 4/7] =?UTF-8?q?Don't=20cancel=20the=20subscription=20until?= =?UTF-8?q?=20after=20we've=20waited=20for=20it=20=F0=9F=A4=A6=E2=80=8D?= =?UTF-8?q?=E2=99=80=EF=B8=8F=20=F0=9F=A4=A6=E2=80=8D=E2=99=80=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index ca25383010..9920f6bfa9 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -471,10 +471,10 @@ class StarWarsSubscriptionTests: XCTestCase { // Now that we've reconnected, this should go through to the same subscription. sendReview() - // Cancel subscription so it doesn't keep receiving from other tests. - sub.cancel() - self.wait(for: [subscriptionExpectation], timeout: 10) + + // Cancel subscription so it doesn't keep receiving from other tests. + sub.cancel() } } From 10c352495a9c4ad5432fee605c77f21a23c93595 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Fri, 31 Jul 2020 13:44:25 -0500 Subject: [PATCH 5/7] wait for the subscription to restart --- Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index 9920f6bfa9..4c82fac421 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -467,6 +467,7 @@ class StarWarsSubscriptionTests: XCTestCase { self.reconnectedExpectation = self.expectation(description: "Web socket reconnected") webSocketTransport.resumeWebSocketConnection() self.wait(for: [self.reconnectedExpectation!], timeout: 10) + self.waitForSubscriptionsToStart() // Now that we've reconnected, this should go through to the same subscription. sendReview() From 5f424b4d7188ff8d99e10b8d54b2fb67fde1523e Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 6 Aug 2020 09:56:25 -0500 Subject: [PATCH 6/7] Remove unnecessary type specification Co-authored-by: Kristaps Grinbergs --- Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index 4c82fac421..39d5fc44b2 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -6,7 +6,7 @@ import StarWarsAPI import Starscream class StarWarsSubscriptionTests: XCTestCase { - let SERVER: String = "ws://localhost:8080/websocket" + let SERVER = "ws://localhost:8080/websocket" let concurrentQueue = DispatchQueue(label: "com.apollographql.testing", attributes: .concurrent) var client: ApolloClient! From ab34ab9ff87162ff87a53f970f604e5d3ec3af16 Mon Sep 17 00:00:00 2001 From: Ellen Shapiro Date: Thu, 6 Aug 2020 10:00:35 -0500 Subject: [PATCH 7/7] clearer naming for subscription --- Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index 39d5fc44b2..ec3c40cc5d 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -438,7 +438,7 @@ class StarWarsSubscriptionTests: XCTestCase { let subscriptionExpectation = self.expectation(description: "Received review") // This should get hit twice - once before we pause the web socket and once after. subscriptionExpectation.expectedFulfillmentCount = 2 - let sub = self.client.subscribe(subscription: subscription) { subscriptionResult in + let reviewAddedSubscription = self.client.subscribe(subscription: subscription) { subscriptionResult in switch subscriptionResult { case .success(let graphQLResult): XCTAssertEqual(graphQLResult.data?.reviewAdded?.episode, .jedi) @@ -475,7 +475,7 @@ class StarWarsSubscriptionTests: XCTestCase { self.wait(for: [subscriptionExpectation], timeout: 10) // Cancel subscription so it doesn't keep receiving from other tests. - sub.cancel() + reviewAddedSubscription.cancel() } }