From 9baff8d9b7b55d6e6542124030c2a0307dc89063 Mon Sep 17 00:00:00 2001 From: Corey Date: Wed, 23 Jun 2021 11:50:39 -0400 Subject: [PATCH 01/14] WIP --- .../LiveQuery/LiveQuerySocket.swift | 9 +++++++++ .../ParseSwift/LiveQuery/ParseLiveQuery.swift | 19 +++++++++++++++++++ .../Protocols/ParseLiveQueryDelegate.swift | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift b/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift index d0914519..ed381d12 100644 --- a/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift +++ b/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift @@ -104,6 +104,15 @@ extension LiveQuerySocket { } } +// MARK: Ping +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +extension LiveQuerySocket { + + func sendPing(_ task: URLSessionWebSocketTask, pongReceiveHandler: @escaping (Error?) -> Void) { + task.sendPing(pongReceiveHandler: pongReceiveHandler) + } +} + // MARK: URLSession @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension URLSession { diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 2edc7fbf..20ece905 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -529,6 +529,25 @@ extension ParseLiveQuery { } } + /// Manually disconnect all sessions and subscriptions from the `ParseLiveQuery` Server. + public func closeAll() { + synchronizationQueue.sync { + URLSession.liveQuery.closeAll() + } + } + + /** + Sends a ping frame from the client side, with a closure to receive the pong from the server endpoint. + - parameter pongReceiveHandler: A closure called by the task when it receives the pong + from the server. The closure receives an `Error` that indicates a lost connection or other problem, + or nil if no error occurred. + */ + public func sendPing(pongReceiveHandler: @escaping (Error?) -> Void) { + synchronizationQueue.sync { + URLSession.liveQuery.sendPing(task, pongReceiveHandler: pongReceiveHandler) + } + } + func close(useDedicatedQueue: Bool) { if useDedicatedQueue { synchronizationQueue.async { diff --git a/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift b/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift index 9a0aac29..e2de9751 100644 --- a/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift +++ b/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift @@ -13,7 +13,7 @@ import FoundationNetworking // swiftlint:disable line_length -///Receive/respond to notifications from the ParseLiveQuery Server. +/// Receive/respond to notifications from the ParseLiveQuery Server. @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public protocol ParseLiveQueryDelegate: AnyObject { From c120a32d69a9a8a127ed0da069f5fdeafd6aee47 Mon Sep 17 00:00:00 2001 From: Corey Date: Wed, 23 Jun 2021 21:05:26 -0400 Subject: [PATCH 02/14] Catch missing errors --- Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift | 3 +++ Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift | 14 +++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift b/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift index ed381d12..b2ed3ac6 100644 --- a/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift +++ b/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift @@ -53,6 +53,9 @@ extension LiveQuerySocket { .encode(StandardMessage(operation: .connect, additionalProperties: true)) guard let encodedAsString = String(data: encoded, encoding: .utf8) else { + let error = ParseError(code: .unknownError, + message: "Couldn't encode connect message: \(encoded)") + completion(error) return } task.send(.string(encodedAsString)) { error in diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 20ece905..d2b3d7d9 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -502,10 +502,15 @@ extension ParseLiveQuery { return } if isSocketEstablished { - try? URLSession.liveQuery.connect(task: self.task) { error in - if error == nil { - self.isConnecting = true + do { + try URLSession.liveQuery.connect(task: self.task) { error in + if error == nil { + self.isConnecting = true + } } + completion(nil) + } catch { + completion(error) } } else { self.synchronizationQueue @@ -513,6 +518,9 @@ extension ParseLiveQuery { .seconds(reconnectInterval)) { self.createTask() self.attempts += 1 + let error = ParseError(code: .unknownError, + message: "Attempted to open socket \(self.attempts)") + completion(error) } } } From 385ef3f36b718cdf37d6846c674550a05b3c773b Mon Sep 17 00:00:00 2001 From: Corey Date: Thu, 24 Jun 2021 18:55:15 -0400 Subject: [PATCH 03/14] Add some publishers for LiveQuery --- ParseSwift.xcodeproj/project.pbxproj | 10 ++++ .../LiveQuery/ParseLiveQuery+combine.swift | 51 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 2a2ea96a..b71f10d6 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -384,6 +384,10 @@ 91678706259BC5D400BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; }; 91678710259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; }; 9167871A259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */; }; + 918CED592684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; }; + 918CED5A2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; }; + 918CED5B2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; }; + 918CED5C2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; }; 9194657824F16E330070296B /* ParseACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ParseACLTests.swift */; }; 91B40651267A66ED00B129CD /* ParseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B40650267A66ED00B129CD /* ParseErrorTests.swift */; }; 91B40652267A66ED00B129CD /* ParseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B40650267A66ED00B129CD /* ParseErrorTests.swift */; }; @@ -707,6 +711,7 @@ 9158916A256A07DD0024BE9A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloud.swift; sourceTree = ""; }; 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudTests.swift; sourceTree = ""; }; + 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseLiveQuery+combine.swift"; sourceTree = ""; }; 9194657724F16E330070296B /* ParseACLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseACLTests.swift; sourceTree = ""; }; 91B40650267A66ED00B129CD /* ParseErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseErrorTests.swift; sourceTree = ""; }; 91CB9536265966DF0043E5D6 /* ParseAnanlyticsCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnanlyticsCombineTests.swift; sourceTree = ""; }; @@ -1048,6 +1053,7 @@ 7003959425A10DFC0052CB31 /* Messages.swift */, 700395A225A119430052CB31 /* Operations.swift */, 7003960825A184EF0052CB31 /* ParseLiveQuery.swift */, + 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */, 700395B925A1470F0052CB31 /* Subscription.swift */, 705D950725BE4C08003EF6F8 /* SubscriptionCallback.swift */, 700395DE25A147C40052CB31 /* Protocols */, @@ -1651,6 +1657,7 @@ 7044C1C825C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF125B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7325BB43EB00A42E7C /* BaseConfig.swift in Sources */, + 918CED592684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */, 70386A0625D9718C0048EC1B /* Data+hexString.swift in Sources */, F97B465F24D9C7B500F4A88B /* KeychainStore.swift in Sources */, 70170A442656B02D0070C905 /* ParseAnalytics.swift in Sources */, @@ -1805,6 +1812,7 @@ 7044C1C925C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF225B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7425BB43EB00A42E7C /* BaseConfig.swift in Sources */, + 918CED5A2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */, 70386A0725D9718C0048EC1B /* Data+hexString.swift in Sources */, F97B466024D9C7B500F4A88B /* KeychainStore.swift in Sources */, 70170A452656B02D0070C905 /* ParseAnalytics.swift in Sources */, @@ -2030,6 +2038,7 @@ 7044C1CB25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF425B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7625BB43EB00A42E7C /* BaseConfig.swift in Sources */, + 918CED5C2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */, 70386A0925D9718C0048EC1B /* Data+hexString.swift in Sources */, F97B460524D9C6F200F4A88B /* NoBody.swift in Sources */, 70170A472656B02D0070C905 /* ParseAnalytics.swift in Sources */, @@ -2122,6 +2131,7 @@ 7044C1CA25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF325B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7525BB43EB00A42E7C /* BaseConfig.swift in Sources */, + 918CED5B2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */, 70386A0825D9718C0048EC1B /* Data+hexString.swift in Sources */, F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */, 70170A462656B02D0070C905 /* ParseAnalytics.swift in Sources */, diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift new file mode 100644 index 00000000..3b67e964 --- /dev/null +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift @@ -0,0 +1,51 @@ +// +// ParseLiveQuery+combine.swift +// ParseSwift +// +// Created by Corey Baker on 6/24/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +extension ParseLiveQuery { + // MARK: Functions - Combine + + /** + Manually establish a connection to the `ParseLiveQuery` Server.. Publishes when established. + - parameter isUserWantsToConnect: Specifies if the user is calling this function. Defaults to `true`. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + public func openPublisher(isUserWantsToConnect: Bool = true) -> Future { + Future { promise in + self.open(isUserWantsToConnect: isUserWantsToConnect) { error in + guard let error = error else { + promise(.success(())) + return + } + promise(.failure(error)) + } + } + } + + /** + Sends a ping frame from the client side. Publishes when a pong is received from the + server endpoint. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + public func sendPingPublisher() -> Future { + Future { promise in + self.sendPing { error in + guard let error = error else { + promise(.success(())) + return + } + promise(.failure(error)) + } + } + } +} +#endif From ed8dcedf830c551cee9f8c2a65b748e953839889 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Thu, 24 Jun 2021 19:57:25 -0400 Subject: [PATCH 04/14] Make ParseLiveQuery.client public --- .../Contents.swift | 26 +++++++++++++++++-- .../ParseSwift/LiveQuery/ParseLiveQuery.swift | 2 +- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift index c09cb9ab..2af655f9 100644 --- a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift @@ -10,7 +10,12 @@ import PlaygroundSupport import Foundation import ParseSwift +#if canImport(SwiftUI) import SwiftUI +#if canImport(Combine) +import Combine +#endif +#endif PlaygroundPage.current.needsIndefiniteExecution = true initializeParse() @@ -39,7 +44,8 @@ struct GameScore: ParseObject { //: Create a query just as you normally would. var query = GameScore.query("score" > 9) - +/* +#if canImport(SwiftUI) //: To use subscriptions inside of SwiftUI struct ContentView: View { @@ -93,7 +99,8 @@ struct ContentView: View { } PlaygroundPage.current.setLiveView(ContentView()) - +#endif +*/ //: This is how you subscribe to your created query using callbacks. let subscription = query.subscribeCallback! @@ -141,6 +148,21 @@ do { print(error) } +//: Ping the LiveQuery server +ParseLiveQuery.client?.sendPing { error in + if let error = error { + print("Error pinging LiveQuery server: \(error)") + } else { + print("Successfully pinged server!") + } +} + +//: To close the current LiveQuery connection. +ParseLiveQuery.client?.close() + +//: To close all LiveQuery connections. +ParseLiveQuery.client?.closeAll() + //: Create a new query. var query2 = GameScore.query("score" > 50) diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index d2b3d7d9..f0d41072 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -213,7 +213,7 @@ public final class ParseLiveQuery: NSObject { @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { - static var client = try? ParseLiveQuery() + public private(set) static var client = try? ParseLiveQuery() var reconnectInterval: Int { let min = NSDecimalNumber(decimal: Swift.min(30, pow(2, attempts) - 1)) From babfe30f64252d2e08b2969da4a86925c0b3fac0 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Thu, 24 Jun 2021 22:40:19 -0400 Subject: [PATCH 05/14] Don't reuse web socket task after it's been closed --- .../ParseSwift/LiveQuery/ParseLiveQuery.swift | 26 +++- .../ParseSwiftTests/ParseLiveQueryTests.swift | 115 +++++++++++++++--- 2 files changed, 123 insertions(+), 18 deletions(-) diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index f0d41072..5406f9cd 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -53,7 +53,13 @@ public final class ParseLiveQuery: NSObject { let notificationQueue: DispatchQueue //Task - var task: URLSessionWebSocketTask! + var task: URLSessionWebSocketTask! { + willSet { + if newValue == nil && isSocketEstablished == true { + isSocketEstablished = false + } + } + } var url: URL! var clientId: String! var attempts: Int = 1 { @@ -205,7 +211,11 @@ public final class ParseLiveQuery: NSObject { close(useDedicatedQueue: false) authenticationDelegate = nil receiveDelegate = nil - URLSession.liveQuery.delegates.removeValue(forKey: task) + if task != nil { + URLSession.liveQuery.delegates.removeValue(forKey: task) + } else { + task = nil + } } } @@ -213,6 +223,7 @@ public final class ParseLiveQuery: NSObject { @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { + /// Current LiveQuery client. public private(set) static var client = try? ParseLiveQuery() var reconnectInterval: Int { @@ -533,7 +544,10 @@ extension ParseLiveQuery { self.task.cancel() self.isDisconnectedByUser = true } - URLSession.liveQuery.delegates.removeValue(forKey: self.task) + if task != nil { + URLSession.liveQuery.delegates.removeValue(forKey: self.task) + } + self.task = nil } } @@ -568,7 +582,9 @@ extension ParseLiveQuery { if self.isConnected { self.task.cancel() } - URLSession.liveQuery.delegates.removeValue(forKey: self.task) + if self.task != nil { + URLSession.liveQuery.delegates.removeValue(forKey: self.task) + } } } @@ -577,6 +593,8 @@ extension ParseLiveQuery { self.pendingSubscriptions.append((requestId, record)) if self.isConnected { URLSession.liveQuery.send(record.messageData, task: self.task, completion: completion) + } else { + self.open(completion: completion) } } } diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index c91f7770..b6851d06 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -50,7 +50,7 @@ class ParseLiveQueryTests: XCTestCase { masterKey: "masterKey", serverURL: url, testing: true) - ParseLiveQuery.client = try? ParseLiveQuery(isDefault: true) + ParseLiveQuery.setDefault(try ParseLiveQuery(isDefault: true)) } override func tearDownWithError() throws { @@ -314,16 +314,20 @@ class ParseLiveQueryTests: XCTestCase { XCTFail("Should be able to get client") return } - client.isSocketEstablished = true // Socket neets to be true + client.isSocketEstablished = true // Socket needs to be true client.isConnecting = true client.attempts = 50 client.isConnected = true client.clientId = "yolo" - - XCTAssertEqual(client.isSocketEstablished, true) - XCTAssertEqual(client.isConnecting, false) - XCTAssertEqual(client.clientId, "yolo") - XCTAssertEqual(client.attempts, 1) + let expectation1 = XCTestExpectation(description: "Synch") + client.synchronizationQueue.sync { + XCTAssertEqual(client.isSocketEstablished, true) + XCTAssertEqual(client.isConnecting, false) + XCTAssertEqual(client.clientId, "yolo") + XCTAssertEqual(client.attempts, 1) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) } func testDisconnectedState() throws { @@ -331,7 +335,7 @@ class ParseLiveQueryTests: XCTestCase { XCTFail("Should be able to get client") return } - client.isSocketEstablished = true // Socket neets to be true + client.isSocketEstablished = true // Socket needs to be true client.isConnecting = true client.isConnected = true client.clientId = "yolo" @@ -352,7 +356,7 @@ class ParseLiveQueryTests: XCTestCase { XCTFail("Should be able to get client") return } - client.isSocketEstablished = true // Socket neets to be true + client.isSocketEstablished = true // Socket needs to be true client.isConnecting = true client.isConnected = true client.clientId = "yolo" @@ -384,11 +388,13 @@ class ParseLiveQueryTests: XCTestCase { XCTAssertEqual(client.clientId, "yolo") client.close() - XCTAssertEqual(client.isSocketEstablished, true) - XCTAssertEqual(client.isConnected, false) - XCTAssertEqual(client.isConnecting, false) - XCTAssertNil(client.clientId) - XCTAssertEqual(client.isDisconnectedByUser, true) + DispatchQueue.main.asyncAfter(deadline: .now()) { + XCTAssertEqual(client.isSocketEstablished, true) + XCTAssertEqual(client.isConnected, false) + XCTAssertEqual(client.isConnecting, false) + XCTAssertNil(client.clientId) + XCTAssertEqual(client.isDisconnectedByUser, true) + } } func testReconnectInterval() throws { @@ -584,6 +590,87 @@ class ParseLiveQueryTests: XCTestCase { wait(for: [expectation1, expectation2], timeout: 20.0) } + func testSubscribeCloseSubscribe() throws { + let query = GameScore.query("score" > 9) + let handler = SubscriptionCallback(query: query) + var subscription = try Query.subscribe(handler) + + guard let client = ParseLiveQuery.getDefault() else { + XCTFail("Should be able to get client") + return + } + XCTAssertEqual(subscription.query, query) + + let expectation1 = XCTestExpectation(description: "Subscribe Handler") + let expectation2 = XCTestExpectation(description: "Resubscribe Handler") + var count = 0 + var originalTask: URLSessionWebSocketTask? + subscription.handleSubscribe { subscribedQuery, isNew in + XCTAssertEqual(query, subscribedQuery) + if count == 0 { + XCTAssertTrue(isNew) + XCTAssertEqual(client.pendingSubscriptions.count, 0) + XCTAssertEqual(client.subscriptions.count, 1) + XCTAssertNotNil(ParseLiveQuery.client?.task) + originalTask = ParseLiveQuery.client?.task + expectation1.fulfill() + } else if count == 2 { + XCTAssertNotNil(ParseLiveQuery.client?.task) + XCTAssertFalse(originalTask == ParseLiveQuery.client?.task) + expectation2.fulfill() + return + } + + ParseLiveQuery.client?.close() + ParseLiveQuery.client?.synchronizationQueue.sync { + XCTAssertNil(ParseLiveQuery.client?.task) + if let socketEstablished = ParseLiveQuery.client?.isSocketEstablished { + XCTAssertFalse(socketEstablished) + } else { + XCTFail("Should have socket that isn't established") + } + + //Resubscribe + do { + count += 1 + subscription = try Query.subscribe(handler) + } catch { + XCTFail("\(error)") + } + + try? self.pretendToBeConnected() + + let response2 = PreliminaryMessageResponse(op: .subscribed, + requestId: 2, + clientId: "yolo", + installationId: "naw") + guard let encoded2 = try? ParseCoding.jsonEncoder().encode(response2) else { + expectation2.fulfill() + return + } + client.received(encoded2) + } + } + + XCTAssertFalse(try client.isSubscribed(query)) + XCTAssertTrue(try client.isPendingSubscription(query)) + XCTAssertEqual(client.subscriptions.count, 0) + XCTAssertEqual(client.pendingSubscriptions.count, 1) + try pretendToBeConnected() + let response = PreliminaryMessageResponse(op: .subscribed, + requestId: 1, + clientId: "yolo", + installationId: "naw") + let encoded = try ParseCoding.jsonEncoder().encode(response) + client.received(encoded) + XCTAssertTrue(try client.isSubscribed(query)) + XCTAssertFalse(try client.isPendingSubscription(query)) + XCTAssertEqual(client.subscriptions.count, 1) + XCTAssertEqual(client.pendingSubscriptions.count, 0) + + wait(for: [expectation1, expectation2], timeout: 20.0) + } + func testServerRedirectResponse() throws { guard let client = ParseLiveQuery.getDefault() else { XCTFail("Should be able to get client") From 1ca97e1efbbe6c7d8323f0ac830c02c9615cc98b Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Thu, 24 Jun 2021 22:43:00 -0400 Subject: [PATCH 06/14] remove asyncs --- .../ParseSwiftTests/ParseLiveQueryTests.swift | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index b6851d06..7d6d8f2c 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -314,20 +314,16 @@ class ParseLiveQueryTests: XCTestCase { XCTFail("Should be able to get client") return } - client.isSocketEstablished = true // Socket needs to be true + client.isSocketEstablished = true // Socket neets to be true client.isConnecting = true client.attempts = 50 client.isConnected = true client.clientId = "yolo" - let expectation1 = XCTestExpectation(description: "Synch") - client.synchronizationQueue.sync { - XCTAssertEqual(client.isSocketEstablished, true) - XCTAssertEqual(client.isConnecting, false) - XCTAssertEqual(client.clientId, "yolo") - XCTAssertEqual(client.attempts, 1) - expectation1.fulfill() - } - wait(for: [expectation1], timeout: 20.0) + + XCTAssertEqual(client.isSocketEstablished, true) + XCTAssertEqual(client.isConnecting, false) + XCTAssertEqual(client.clientId, "yolo") + XCTAssertEqual(client.attempts, 1) } func testDisconnectedState() throws { @@ -335,7 +331,7 @@ class ParseLiveQueryTests: XCTestCase { XCTFail("Should be able to get client") return } - client.isSocketEstablished = true // Socket needs to be true + client.isSocketEstablished = true // Socket neets to be true client.isConnecting = true client.isConnected = true client.clientId = "yolo" @@ -356,7 +352,7 @@ class ParseLiveQueryTests: XCTestCase { XCTFail("Should be able to get client") return } - client.isSocketEstablished = true // Socket needs to be true + client.isSocketEstablished = true // Socket neets to be true client.isConnecting = true client.isConnected = true client.clientId = "yolo" @@ -388,13 +384,11 @@ class ParseLiveQueryTests: XCTestCase { XCTAssertEqual(client.clientId, "yolo") client.close() - DispatchQueue.main.asyncAfter(deadline: .now()) { - XCTAssertEqual(client.isSocketEstablished, true) - XCTAssertEqual(client.isConnected, false) - XCTAssertEqual(client.isConnecting, false) - XCTAssertNil(client.clientId) - XCTAssertEqual(client.isDisconnectedByUser, true) - } + XCTAssertEqual(client.isSocketEstablished, true) + XCTAssertEqual(client.isConnected, false) + XCTAssertEqual(client.isConnecting, false) + XCTAssertNil(client.clientId) + XCTAssertEqual(client.isDisconnectedByUser, true) } func testReconnectInterval() throws { From 4ffc808c35d4887ede26b6bb2ba510b40d3b9d8a Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Fri, 25 Jun 2021 07:58:39 -0400 Subject: [PATCH 07/14] Fix broken testcases --- Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift | 7 +------ Tests/ParseSwiftTests/ParseLiveQueryTests.swift | 7 ++++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 5406f9cd..3688339d 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -211,11 +211,6 @@ public final class ParseLiveQuery: NSObject { close(useDedicatedQueue: false) authenticationDelegate = nil receiveDelegate = nil - if task != nil { - URLSession.liveQuery.delegates.removeValue(forKey: task) - } else { - task = nil - } } } @@ -518,8 +513,8 @@ extension ParseLiveQuery { if error == nil { self.isConnecting = true } + completion(error) } - completion(nil) } catch { completion(error) } diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index 7d6d8f2c..a2f1b852 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -316,14 +316,15 @@ class ParseLiveQueryTests: XCTestCase { } client.isSocketEstablished = true // Socket neets to be true client.isConnecting = true - client.attempts = 50 client.isConnected = true + client.attempts = 5 client.clientId = "yolo" + client.isDisconnectedByUser = false XCTAssertEqual(client.isSocketEstablished, true) XCTAssertEqual(client.isConnecting, false) XCTAssertEqual(client.clientId, "yolo") - XCTAssertEqual(client.attempts, 1) + XCTAssertEqual(client.attempts, 5) } func testDisconnectedState() throws { @@ -384,7 +385,7 @@ class ParseLiveQueryTests: XCTestCase { XCTAssertEqual(client.clientId, "yolo") client.close() - XCTAssertEqual(client.isSocketEstablished, true) + XCTAssertEqual(client.isSocketEstablished, false) XCTAssertEqual(client.isConnected, false) XCTAssertEqual(client.isConnecting, false) XCTAssertNil(client.clientId) From ae536ca4017b9b049d090bc59d9c13b1360187b6 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Fri, 25 Jun 2021 08:07:59 -0400 Subject: [PATCH 08/14] Check for open socket before sending ping --- Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 3688339d..032bc189 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -525,7 +525,7 @@ extension ParseLiveQuery { self.createTask() self.attempts += 1 let error = ParseError(code: .unknownError, - message: "Attempted to open socket \(self.attempts)") + message: "Attempted to open socket \(self.attempts) time(s)") completion(error) } } @@ -561,7 +561,13 @@ extension ParseLiveQuery { */ public func sendPing(pongReceiveHandler: @escaping (Error?) -> Void) { synchronizationQueue.sync { - URLSession.liveQuery.sendPing(task, pongReceiveHandler: pongReceiveHandler) + if isSocketEstablished { + URLSession.liveQuery.sendPing(task, pongReceiveHandler: pongReceiveHandler) + } else { + let error = ParseError(code: .unknownError, + message: "Need to open the websocket before it can be pinged.") + pongReceiveHandler(error) + } } } From 63e309b8b4bf6d5c15724a96abc7d52a548968fc Mon Sep 17 00:00:00 2001 From: Corey Date: Fri, 25 Jun 2021 13:50:27 -0400 Subject: [PATCH 09/14] Add more test cases for livequery --- ParseSwift.xcodeproj/project.pbxproj | 8 + .../ParseSwift/LiveQuery/ParseLiveQuery.swift | 10 ++ .../ParseLiveQueryCombineTests.swift | 139 ++++++++++++++++++ .../ParseSwiftTests/ParseLiveQueryTests.swift | 86 ++++++++++- 4 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 Tests/ParseSwiftTests/ParseLiveQueryCombineTests.swift diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index b71f10d6..c2a79e1c 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -388,6 +388,9 @@ 918CED5A2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; }; 918CED5B2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; }; 918CED5C2684C74000CFDC83 /* ParseLiveQuery+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */; }; + 918CED5E268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */; }; + 918CED5F268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */; }; + 918CED60268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */; }; 9194657824F16E330070296B /* ParseACLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9194657724F16E330070296B /* ParseACLTests.swift */; }; 91B40651267A66ED00B129CD /* ParseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B40650267A66ED00B129CD /* ParseErrorTests.swift */; }; 91B40652267A66ED00B129CD /* ParseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B40650267A66ED00B129CD /* ParseErrorTests.swift */; }; @@ -712,6 +715,7 @@ 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloud.swift; sourceTree = ""; }; 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudTests.swift; sourceTree = ""; }; 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseLiveQuery+combine.swift"; sourceTree = ""; }; + 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLiveQueryCombineTests.swift; sourceTree = ""; }; 9194657724F16E330070296B /* ParseACLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseACLTests.swift; sourceTree = ""; }; 91B40650267A66ED00B129CD /* ParseErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseErrorTests.swift; sourceTree = ""; }; 91CB9536265966DF0043E5D6 /* ParseAnanlyticsCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnanlyticsCombineTests.swift; sourceTree = ""; }; @@ -894,6 +898,7 @@ 70386A5B25D9A4010048EC1B /* ParseLDAPCombineTests.swift */, 70386A4525D99C8B0048EC1B /* ParseLDAPTests.swift */, 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */, + 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */, 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */, 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */, 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift */, @@ -1742,6 +1747,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 918CED5E268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */, 911DB13624C4FC100027F3C7 /* ParseObjectTests.swift in Sources */, 70E09E1C262F0634002DD451 /* ParsePointerCombineTests.swift in Sources */, 89899D592603CF3E002E2043 /* ParseTwitterTests.swift in Sources */, @@ -1906,6 +1912,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 918CED60268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */, 709B98512556ECAA00507778 /* ParseEncoderExtraTests.swift in Sources */, 70E09E1E262F0634002DD451 /* ParsePointerCombineTests.swift in Sources */, 89899D642603CF3F002E2043 /* ParseTwitterTests.swift in Sources */, @@ -1968,6 +1975,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 918CED5F268618C600CFDC83 /* ParseLiveQueryCombineTests.swift in Sources */, 70F2E2B6254F283000B2EA5C /* ParseACLTests.swift in Sources */, 70E09E1D262F0634002DD451 /* ParsePointerCombineTests.swift in Sources */, 89899D632603CF3E002E2043 /* ParseTwitterTests.swift in Sources */, diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 032bc189..64684c42 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -243,6 +243,15 @@ extension ParseLiveQuery { //Remove in subscriptions just in case the server //responded before this was called self.subscriptions.removeValue(forKey: requestIdToRemove) + closeWebsocketIfNoSubscriptions() + } + + func closeWebsocketIfNoSubscriptions() { + self.notificationQueue.async { + if self.subscriptions.isEmpty && self.pendingSubscriptions.isEmpty { + self.close() + } + } } /// Set a specific ParseLiveQuery client to be the default for all `ParseLiveQuery` connections. @@ -288,6 +297,7 @@ extension ParseLiveQuery { public func removePendingSubscription(_ query: Query) throws { let queryData = try ParseCoding.jsonEncoder().encode(query) pendingSubscriptions.removeAll(where: { (_, value) -> Bool in + self.closeWebsocketIfNoSubscriptions() if queryData == value.queryData { return true } else { diff --git a/Tests/ParseSwiftTests/ParseLiveQueryCombineTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryCombineTests.swift new file mode 100644 index 00000000..538c9fbf --- /dev/null +++ b/Tests/ParseSwiftTests/ParseLiveQueryCombineTests.swift @@ -0,0 +1,139 @@ +// +// ParseLiveQueryCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 6/25/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if !os(Linux) && !os(Android) +import Foundation +import XCTest +@testable import ParseSwift +#if canImport(Combine) +import Combine +#endif + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseLiveQueryCombineTests: XCTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + ParseLiveQuery.setDefault(try ParseLiveQuery(isDefault: true)) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + #if !os(Linux) && !os(Android) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + URLSession.liveQuery.closeAll() + } + + func testOpen() throws { + guard let client = ParseLiveQuery.getDefault() else { + XCTFail("Should be able to get client") + return + } + client.close() + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Send Ping") + let publisher = client.openPublisher(isUserWantsToConnect: true) + .sink(receiveCompletion: { result in + + switch result { + + case .finished: + XCTFail("Should have produced failure") + case .failure(let error): + XCTAssertNotNil(error) //Should always fail since WS isn't intercepted. + } + expectation1.fulfill() + + }, receiveValue: { _ in + XCTFail("Should have produced error") + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testPingSocketNotEstablished() throws { + guard let client = ParseLiveQuery.getDefault() else { + XCTFail("Should be able to get client") + return + } + client.close() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Send Ping") + let publisher = client.sendPingPublisher() + .sink(receiveCompletion: { result in + + switch result { + + case .finished: + XCTFail("Should have produced failure") + case .failure(let error): + XCTAssertEqual(client.isSocketEstablished, false) + XCTAssertNil(client.task) + guard let parseError = error as? ParseError else { + XCTFail("Should have casted to ParseError.") + expectation1.fulfill() + return + } + XCTAssertEqual(parseError.code, ParseError.Code.unknownError) + XCTAssertTrue(parseError.message.contains("pinged")) + } + expectation1.fulfill() + + }, receiveValue: { _ in + XCTFail("Should have produced error") + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testPing() throws { + guard let client = ParseLiveQuery.getDefault() else { + XCTFail("Should be able to get client") + return + } + client.isSocketEstablished = true // Socket needs to be true + client.isConnecting = true + client.isConnected = true + client.clientId = "yolo" + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Send Ping") + let publisher = client.sendPingPublisher() + .sink(receiveCompletion: { result in + + switch result { + + case .finished: + XCTFail("Should have produced failure") + case .failure(let error): + XCTAssertEqual(client.isSocketEstablished, true) + XCTAssertNotNil(error) // Should have error because testcases don't intercept websocket + } + expectation1.fulfill() + + }, receiveValue: { _ in + XCTFail("Should have produced error") + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } +} +#endif diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index a2f1b852..ab7cb546 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -314,7 +314,7 @@ class ParseLiveQueryTests: XCTestCase { XCTFail("Should be able to get client") return } - client.isSocketEstablished = true // Socket neets to be true + client.isSocketEstablished = true // Socket needs to be true client.isConnecting = true client.isConnected = true client.attempts = 5 @@ -325,6 +325,19 @@ class ParseLiveQueryTests: XCTestCase { XCTAssertEqual(client.isConnecting, false) XCTAssertEqual(client.clientId, "yolo") XCTAssertEqual(client.attempts, 5) + + // Test too many attempts and close + client.isSocketEstablished = true // Socket needs to be true + client.isConnecting = true + client.isConnected = true + client.attempts = ParseLiveQueryConstants.maxConnectionAttempts + 1 + client.clientId = "yolo" + client.isDisconnectedByUser = false + + XCTAssertEqual(client.isSocketEstablished, false) + XCTAssertEqual(client.isConnecting, false) + XCTAssertEqual(client.clientId, "yolo") + XCTAssertEqual(client.attempts, ParseLiveQueryConstants.maxConnectionAttempts + 1) } func testDisconnectedState() throws { @@ -332,7 +345,7 @@ class ParseLiveQueryTests: XCTestCase { XCTFail("Should be able to get client") return } - client.isSocketEstablished = true // Socket neets to be true + client.isSocketEstablished = true // Socket needs to be true client.isConnecting = true client.isConnected = true client.clientId = "yolo" @@ -353,7 +366,7 @@ class ParseLiveQueryTests: XCTestCase { XCTFail("Should be able to get client") return } - client.isSocketEstablished = true // Socket neets to be true + client.isSocketEstablished = true // Socket needs to be true client.isConnecting = true client.isConnected = true client.clientId = "yolo" @@ -392,6 +405,70 @@ class ParseLiveQueryTests: XCTestCase { XCTAssertEqual(client.isDisconnectedByUser, true) } + func testOpenSocket() throws { + guard let client = ParseLiveQuery.getDefault() else { + XCTFail("Should be able to get client") + return + } + client.close() + client.open(isUserWantsToConnect: true) { error in + XCTAssertNotNil(error) //Should always fail since WS isn't intercepted. + } + } + + func testCloseAll() throws { + guard let client = ParseLiveQuery.getDefault() else { + XCTFail("Should be able to get client") + return + } + client.closeAll() + client.synchronizationQueue.asyncAfter(deadline: .now() + 2) { + XCTAssertFalse(client.isSocketEstablished) + XCTAssertNil(client.task) + } + } + + func testPingSocketNotEstablished() throws { + guard let client = ParseLiveQuery.getDefault() else { + XCTFail("Should be able to get client") + return + } + client.close() + let expectation1 = XCTestExpectation(description: "Send Ping") + client.sendPing { error in + XCTAssertEqual(client.isSocketEstablished, false) + XCTAssertNil(client.task) + guard let parseError = error as? ParseError else { + XCTFail("Should have casted to ParseError.") + expectation1.fulfill() + return + } + XCTAssertEqual(parseError.code, ParseError.Code.unknownError) + XCTAssertTrue(parseError.message.contains("pinged")) + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testPing() throws { + guard let client = ParseLiveQuery.getDefault() else { + XCTFail("Should be able to get client") + return + } + client.isSocketEstablished = true // Socket needs to be true + client.isConnecting = true + client.isConnected = true + client.clientId = "yolo" + + let expectation1 = XCTestExpectation(description: "Send Ping") + client.sendPing { error in + XCTAssertEqual(client.isSocketEstablished, true) + XCTAssertNotNil(error) // Should have error because testcases don't intercept websocket + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testReconnectInterval() throws { guard let client = ParseLiveQuery.getDefault() else { XCTFail("Should be able to get client") @@ -546,6 +623,9 @@ class ParseLiveQueryTests: XCTestCase { //Unsubscribe subscription.handleUnsubscribe { query in XCTAssertEqual(query, subscribedQuery) + XCTAssertTrue(client.pendingSubscriptions.isEmpty) + XCTAssertTrue(client.subscriptions.isEmpty) + XCTAssertFalse(client.isSocketEstablished) expectation2.fulfill() } XCTAssertNotNil(try? query.unsubscribe()) From 73fa31ec21730963a3eb150a06cd4642809dfbb3 Mon Sep 17 00:00:00 2001 From: Corey Date: Fri, 25 Jun 2021 14:31:13 -0400 Subject: [PATCH 10/14] Add changelog entry --- CHANGELOG.md | 3 +++ ParseSwift.xcodeproj/project.pbxproj | 4 ++-- Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift | 2 ++ Tests/ParseSwiftTests/ParseLiveQueryTests.swift | 3 +-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49569114..14ba8788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.8.3...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +__Fixes__ +- Fixed a bug in LiveQuery that prevented reconnecting after a connection was closed. Also added a sendPing method to LiveQuery ([#172](https://github.com/parse-community/Parse-Swift/pull/172)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 1.8.3 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.8.2...1.8.3) diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index c2a79e1c..d4755c23 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -897,8 +897,8 @@ 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */, 70386A5B25D9A4010048EC1B /* ParseLDAPCombineTests.swift */, 70386A4525D99C8B0048EC1B /* ParseLDAPTests.swift */, - 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */, 918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */, + 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */, 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */, 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */, 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift */, @@ -1054,11 +1054,11 @@ isa = PBXGroup; children = ( 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */, - 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */, 7003959425A10DFC0052CB31 /* Messages.swift */, 700395A225A119430052CB31 /* Operations.swift */, 7003960825A184EF0052CB31 /* ParseLiveQuery.swift */, 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */, + 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */, 700395B925A1470F0052CB31 /* Subscription.swift */, 705D950725BE4C08003EF6F8 /* SubscriptionCallback.swift */, 700395DE25A147C40052CB31 /* Protocols */, diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 64684c42..0833dc4a 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -588,6 +588,7 @@ extension ParseLiveQuery { self.task.cancel() } URLSession.liveQuery.delegates.removeValue(forKey: self.task) + self.task = nil } } else { if self.isConnected { @@ -596,6 +597,7 @@ extension ParseLiveQuery { if self.task != nil { URLSession.liveQuery.delegates.removeValue(forKey: self.task) } + self.task = nil } } diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index ab7cb546..d0d97bbd 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -751,9 +751,8 @@ class ParseLiveQueryTests: XCTestCase { XCTFail("Should be able to get client") return } - try pretendToBeConnected() - guard let url = URL(string: "http://parse.com") else { + guard let url = URL(string: "wss://parse.com") else { XCTFail("should create url") return } From 2c664440fa9e38288a79f7c92db8c782673016af Mon Sep 17 00:00:00 2001 From: Corey Date: Fri, 25 Jun 2021 15:28:58 -0400 Subject: [PATCH 11/14] nits --- Tests/ParseSwiftTests/ParseLiveQueryTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index d0d97bbd..1f716639 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -1416,9 +1416,9 @@ class ParseLiveQueryTests: XCTestCase { try pretendToBeConnected() let response = PreliminaryMessageResponse(op: .subscribed, - requestId: 1, - clientId: "yolo", - installationId: "naw") + requestId: 1, + clientId: "yolo", + installationId: "naw") let encoded = try ParseCoding.jsonEncoder().encode(response) client.received(encoded) From b7109848e51bd757925b9bf4537e132d1f961e4d Mon Sep 17 00:00:00 2001 From: Corey Date: Fri, 25 Jun 2021 15:49:52 -0400 Subject: [PATCH 12/14] Add back playground --- .../Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift index 2af655f9..04dbc429 100644 --- a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift @@ -44,7 +44,7 @@ struct GameScore: ParseObject { //: Create a query just as you normally would. var query = GameScore.query("score" > 9) -/* + #if canImport(SwiftUI) //: To use subscriptions inside of SwiftUI struct ContentView: View { @@ -100,7 +100,7 @@ struct ContentView: View { PlaygroundPage.current.setLiveView(ContentView()) #endif -*/ + //: This is how you subscribe to your created query using callbacks. let subscription = query.subscribeCallback! From 72fc22bc3bb5ae83c31372806ba884f25d3e2869 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Fri, 25 Jun 2021 21:00:19 -0400 Subject: [PATCH 13/14] Add to subscription list before removing from pending list --- .../Contents.swift | 118 +++++++++++++++--- .../ParseSwift/LiveQuery/ParseLiveQuery.swift | 11 +- 2 files changed, 105 insertions(+), 24 deletions(-) diff --git a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift index 04dbc429..cf5fee33 100644 --- a/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/11 - LiveQuery.xcplaygroundpage/Contents.swift @@ -43,7 +43,7 @@ struct GameScore: ParseObject { //: Be sure you have LiveQuery enabled on your server. //: Create a query just as you normally would. -var query = GameScore.query("score" > 9) +var query = GameScore.query("score" < 11) #if canImport(SwiftUI) //: To use subscriptions inside of SwiftUI @@ -61,7 +61,7 @@ struct ContentView: View { Text("Unsubscribed from query!") } else if let event = subscription.event { - //: This is how you register to receive notificaitons of events related to your LiveQuery. + //: This is how you register to receive notifications of events related to your LiveQuery. switch event.event { case .entered(let object): @@ -116,7 +116,7 @@ subscription.handleSubscribe { subscribedQuery, isNew in } } -//: This is how you register to receive notificaitons of events related to your LiveQuery. +//: This is how you register to receive notifications of events related to your LiveQuery. subscription.handleEvent { _, event in switch event { @@ -133,10 +133,19 @@ subscription.handleEvent { _, event in } } +//: Ping the LiveQuery server +ParseLiveQuery.client?.sendPing { error in + if let error = error { + print("Error pinging LiveQuery server: \(error)") + } else { + print("Successfully pinged server!") + } +} + //: Now go to your dashboard, go to the GameScore table and add, update or remove rows. //: You should receive notifications for each. -//: This is how you register to receive notificaitons about being unsubscribed. +//: This is how you register to receive notifications about being unsubscribed. subscription.handleUnsubscribe { query in print("Unsubscribed from \(query)") } @@ -148,7 +157,8 @@ do { print(error) } -//: Ping the LiveQuery server +//: Ping the LiveQuery server. This should produce an error +//: because LiveQuery is disconnected. ParseLiveQuery.client?.sendPing { error in if let error = error { print("Error pinging LiveQuery server: \(error)") @@ -157,12 +167,6 @@ ParseLiveQuery.client?.sendPing { error in } } -//: To close the current LiveQuery connection. -ParseLiveQuery.client?.close() - -//: To close all LiveQuery connections. -ParseLiveQuery.client?.closeAll() - //: Create a new query. var query2 = GameScore.query("score" > 50) @@ -199,11 +203,86 @@ subscription2.handleEvent { _, event in } } -//: Now go to your dashboard, go to the GameScore table and add, update or remove rows. -//: You should receive notifications for each, but only with your fields information. +//: To close the current LiveQuery connection. +ParseLiveQuery.client?.close() + +//: To close all LiveQuery connections use: +//ParseLiveQuery.client?.closeAll() -//: This is how you register to receive notificaitons about being unsubscribed. -subscription2.handleUnsubscribe { query in +//: Ping the LiveQuery server. This should produce an error +//: because LiveQuery is disconnected. +ParseLiveQuery.client?.sendPing { error in + if let error = error { + print("Error pinging LiveQuery server: \(error)") + } else { + print("Successfully pinged server!") + } +} + +//: Subscribe to your new query. +let subscription3 = query2.subscribeCallback! + +//: As before, setup your subscription and event handlers. +subscription3.handleSubscribe { subscribedQuery, isNew in + + //: You can check this subscription is for this query. + if isNew { + print("Successfully subscribed to new query \(subscribedQuery)") + } else { + print("Successfully updated subscription to new query \(subscribedQuery)") + } +} + +subscription3.handleEvent { _, event in + switch event { + + case .entered(let object): + print("Entered: \(object)") + case .left(let object): + print("Left: \(object)") + case .created(let object): + print("Created: \(object)") + case .updated(let object): + print("Updated: \(object)") + case .deleted(let object): + print("Deleted: \(object)") + } +} + +//: Now lets subscribe to an additional query. +let subscription4 = query.subscribeCallback! + +//: This is how you receive notifications about the success +//: of your subscription. +subscription4.handleSubscribe { subscribedQuery, isNew in + + //: You can check this subscription is for this query + if isNew { + print("Successfully subscribed to new query \(subscribedQuery)") + } else { + print("Successfully updated subscription to new query \(subscribedQuery)") + } +} + +//: This is how you register to receive notifications of events related to your LiveQuery. +subscription4.handleEvent { _, event in + switch event { + + case .entered(let object): + print("Entered: \(object)") + case .left(let object): + print("Left: \(object)") + case .created(let object): + print("Created: \(object)") + case .updated(let object): + print("Updated: \(object)") + case .deleted(let object): + print("Deleted: \(object)") + } +} + +//: Now we will will unsubscribe from one of the subsriptions, but maintain the connection. +subscription3.handleUnsubscribe { query in print("Unsubscribed from \(query)") } @@ -214,5 +293,14 @@ do { print(error) } +//: Ping the LiveQuery server +ParseLiveQuery.client?.sendPing { error in + if let error = error { + print("Error pinging LiveQuery server: \(error)") + } else { + print("Successfully pinged server!") + } +} + PlaygroundPage.current.finishExecution() //: [Next](@next) diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 0833dc4a..28877a5d 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -238,11 +238,7 @@ extension ParseLiveQuery { } func removePendingSubscription(_ requestId: Int) { - let requestIdToRemove = RequestId(value: requestId) self.pendingSubscriptions.removeAll(where: { $0.0.value == requestId }) - //Remove in subscriptions just in case the server - //responded before this was called - self.subscriptions.removeValue(forKey: requestIdToRemove) closeWebsocketIfNoSubscriptions() } @@ -422,8 +418,8 @@ extension ParseLiveQuery: LiveQuerySocketDelegate { } else { isNew = true } - self.removePendingSubscription(subscribed.0.value) self.subscriptions[subscribed.0] = subscribed.1 + self.removePendingSubscription(subscribed.0.value) self.notificationQueue.async { subscribed.1.subscribeHandlerClosure?(isNew) } @@ -433,6 +429,7 @@ extension ParseLiveQuery: LiveQuerySocketDelegate { guard let subscription = self.subscriptions[requestId] else { return } + self.subscriptions.removeValue(forKey: requestId) self.removePendingSubscription(preliminaryMessage.requestId) self.notificationQueue.async { subscription.unsubscribeHandlerClosure?() @@ -721,10 +718,6 @@ extension ParseLiveQuery { let updatedRecord = value updatedRecord.messageData = encoded self.send(record: updatedRecord, requestId: key) { _ in } - } else { - let error = ParseError(code: .unknownError, - message: "ParseLiveQuery Error: Not subscribed to this query") - throw error } } } From 221944ebd53a64c6a178c7def069df5f16240554 Mon Sep 17 00:00:00 2001 From: Corey Date: Sat, 26 Jun 2021 10:00:51 -0400 Subject: [PATCH 14/14] Update Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift Co-authored-by: Tom Fox <13188249+TomWFox@users.noreply.github.com> --- Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift index 3b67e964..b889f392 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift @@ -15,7 +15,7 @@ extension ParseLiveQuery { // MARK: Functions - Combine /** - Manually establish a connection to the `ParseLiveQuery` Server.. Publishes when established. + Manually establish a connection to the `ParseLiveQuery` Server. Publishes when established. - parameter isUserWantsToConnect: Specifies if the user is calling this function. Defaults to `true`. - returns: A publisher that eventually produces a single value and then finishes or fails. */