Skip to content

Fix LiveQuery reconnection bug #172

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

Merged
merged 14 commits into from
Jun 26, 2021
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -38,8 +43,9 @@ 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
struct ContentView: View {

Expand All @@ -55,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):
Expand Down Expand Up @@ -93,6 +99,7 @@ struct ContentView: View {
}

PlaygroundPage.current.setLiveView(ContentView())
#endif

//: This is how you subscribe to your created query using callbacks.
let subscription = query.subscribeCallback!
Expand All @@ -109,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 {

Expand All @@ -126,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)")
}
Expand All @@ -141,6 +157,16 @@ do {
print(error)
}

//: 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!")
}
}

//: Create a new query.
var query2 = GameScore.query("score" > 50)

Expand Down Expand Up @@ -177,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()

//: 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)")
}
}

//: This is how you register to receive notificaitons about being unsubscribed.
subscription2.handleUnsubscribe { query in
//: Now we will will unsubscribe from one of the subsriptions, but maintain the connection.
subscription3.handleUnsubscribe { query in
print("Unsubscribed from \(query)")
}

Expand All @@ -192,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)
20 changes: 19 additions & 1 deletion ParseSwift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,13 @@
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 */; };
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 */; };
Expand Down Expand Up @@ -707,6 +714,8 @@
9158916A256A07DD0024BE9A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
916786E1259B7DDA00BB5B4E /* ParseCloud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloud.swift; sourceTree = "<group>"; };
916786EF259BC59600BB5B4E /* ParseCloudTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudTests.swift; sourceTree = "<group>"; };
918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseLiveQuery+combine.swift"; sourceTree = "<group>"; };
918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLiveQueryCombineTests.swift; sourceTree = "<group>"; };
9194657724F16E330070296B /* ParseACLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseACLTests.swift; sourceTree = "<group>"; };
91B40650267A66ED00B129CD /* ParseErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseErrorTests.swift; sourceTree = "<group>"; };
91CB9536265966DF0043E5D6 /* ParseAnanlyticsCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnanlyticsCombineTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -888,6 +897,7 @@
70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */,
70386A5B25D9A4010048EC1B /* ParseLDAPCombineTests.swift */,
70386A4525D99C8B0048EC1B /* ParseLDAPTests.swift */,
918CED5D268618C600CFDC83 /* ParseLiveQueryCombineTests.swift */,
7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */,
70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */,
7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */,
Expand Down Expand Up @@ -1044,10 +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 */,
Expand Down Expand Up @@ -1651,6 +1662,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 */,
Expand Down Expand Up @@ -1735,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 */,
Expand Down Expand Up @@ -1805,6 +1818,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 */,
Expand Down Expand Up @@ -1898,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 */,
Expand Down Expand Up @@ -1960,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 */,
Expand Down Expand Up @@ -2030,6 +2046,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 */,
Expand Down Expand Up @@ -2122,6 +2139,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 */,
Expand Down
12 changes: 12 additions & 0 deletions Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -104,6 +107,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 {
Expand Down
51 changes: 51 additions & 0 deletions Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift
Original file line number Diff line number Diff line change
@@ -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<Void, Error> {
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<Void, Error> {
Future { promise in
self.sendPing { error in
guard let error = error else {
promise(.success(()))
return
}
promise(.failure(error))
}
}
}
}
#endif
Loading