Repository: parse-community/Parse-SDK-JS (JavaScript)

Issue:
Ensure live query subscription unsubscribes
### New Feature / Enhancement Checklist
<!--
    Check every following box [x] before submitting your issue.
    Click the "Preview" tab for better readability.
    Thanks for contributing to Parse Platform!
-->

- [x] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
- [x] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
- [x] I have searched through [existing issues](https://github.com/parse-community/Parse-SDK-JS/issues?q=is%3Aissue).

### Feature / Enhancement Description
<!-- What is the concept of the functionality and how should it be implemented? -->

The SDK ensures that live query subscribes successfully with a promise. The same should be applied to `unsubscribe`.  This could be used in the SDK test suite to ensure connections to the server are closed.

### Example Use Case
<!-- What is an example use case in steps (1. / 2. / 3. / etc.) that describes the functionality? -->

Using a promise would be a breaking change but would be aligned with `subscribe`
```
await subscription.unsubscribe();
```

Using a callback is an alternative
```
subscription.unsubscribe((error) => {});
```

Edit: I think we can support both


Discussion: 
### Thanks for opening this issue!

- ❌ Please edit your post and use the provided template when creating a new issue. This helps everyone to understand your post better and asks for essential information to quicker review the issue.

<!-- parse-issue-bot-meta-tag-id {"requireTemplate":true} -->





Fix (patch):
diff --git a/src/LiveQueryClient.js b/src/LiveQueryClient.js
index 7e8f4d8c4..17b4ab40c 100644
--- a/src/LiveQueryClient.js
+++ b/src/LiveQueryClient.js
@@ -224,19 +224,20 @@ class LiveQueryClient extends EventEmitter {
    * After calling unsubscribe you'll stop receiving events from the subscription object.
    *
    * @param {object} subscription - subscription you would like to unsubscribe from.
+   * @returns {Promise | undefined}
    */
-  unsubscribe(subscription: Object) {
+  unsubscribe(subscription: Object): ?Promise {
     if (!subscription) {
       return;
     }
-
-    this.subscriptions.delete(subscription.id);
     const unsubscribeRequest = {
       op: OP_TYPES.UNSUBSCRIBE,
       requestId: subscription.id,
     };
-    this.connectPromise.then(() => {
-      this.socket.send(JSON.stringify(unsubscribeRequest));
+    return this.connectPromise.then(() => {
+      return this.socket.send(JSON.stringify(unsubscribeRequest));
+    }).then(() => {
+      return subscription.unsubscribePromise;
     });
   }
 
@@ -400,9 +401,14 @@ class LiveQueryClient extends EventEmitter {
       }
       break;
     }
-    case OP_EVENTS.UNSUBSCRIBED:
-      // We have already deleted subscription in unsubscribe(), do nothing here
+    case OP_EVENTS.UNSUBSCRIBED: {
+      if (subscription) {
+        this.subscriptions.delete(data.requestId);
+        subscription.subscribed = false;
+        subscription.unsubscribePromise.resolve();
+      }
       break;
+    }
     default: {
       // create, update, enter, leave, delete cases
       if (!subscription) {
diff --git a/src/LiveQuerySubscription.js b/src/LiveQuerySubscription.js
index 1c6c638ee..503a07175 100644
--- a/src/LiveQuerySubscription.js
+++ b/src/LiveQuerySubscription.js
@@ -99,6 +99,7 @@ class Subscription extends EventEmitter {
     this.query = query;
     this.sessionToken = sessionToken;
     this.subscribePromise = resolvingPromise();
+    this.unsubscribePromise = resolvingPromise();
     this.subscribed = false;
 
     // adding listener so process does not crash
@@ -115,8 +116,8 @@ class Subscription extends EventEmitter {
     return CoreManager.getLiveQueryController()
       .getDefaultLiveQueryClient()
       .then(liveQueryClient => {
-        liveQueryClient.unsubscribe(this);
         this.emit('close');
+        return liveQueryClient.unsubscribe(this);
       });
   }
 }