From 59af46dfc489c1b67ec8147e28b5bb1dbd5c46d6 Mon Sep 17 00:00:00 2001 From: Guillaume VARA Date: Fri, 22 Mar 2019 04:23:33 +0100 Subject: [PATCH 1/5] Fixes LiveQuery behavior on multiple subscriptions --- src/LiveQuerySubscription.js | 1 + src/ParseLiveQuery.js | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/LiveQuerySubscription.js b/src/LiveQuerySubscription.js index b2013a26b..16a81b6b6 100644 --- a/src/LiveQuerySubscription.js +++ b/src/LiveQuerySubscription.js @@ -110,6 +110,7 @@ class Subscription extends EventEmitter { return CoreManager.getLiveQueryController().getDefaultLiveQueryClient().then((liveQueryClient) => { liveQueryClient.unsubscribe(this); this.emit('close'); + return Promise.resolve(this); }); } } diff --git a/src/ParseLiveQuery.js b/src/ParseLiveQuery.js index e00d9a022..20eaf4619 100644 --- a/src/ParseLiveQuery.js +++ b/src/ParseLiveQuery.js @@ -15,12 +15,12 @@ import CoreManager from './CoreManager'; function open() { const LiveQueryController = CoreManager.getLiveQueryController(); - LiveQueryController.open(); + return LiveQueryController.open(); } function close() { const LiveQueryController = CoreManager.getLiveQueryController(); - LiveQueryController.close(); + return LiveQueryController.close(); } /** @@ -149,19 +149,23 @@ const DefaultLiveQueryController = { }); }, open() { - getLiveQueryClient().then((liveQueryClient) => { - return liveQueryClient.open(); + return getLiveQueryClient().then((liveQueryClient) => { + return Promise.resolve( + liveQueryClient.open() + ); }); }, close() { - getLiveQueryClient().then((liveQueryClient) => { - return liveQueryClient.close(); + return getLiveQueryClient().then((liveQueryClient) => { + return Promise.resolve( + liveQueryClient.close() + ); }); }, subscribe(query: any): EventEmitter { const subscriptionWrap = new EventEmitter(); - getLiveQueryClient().then((liveQueryClient) => { + return getLiveQueryClient().then((liveQueryClient) => { if (liveQueryClient.shouldOpen()) { liveQueryClient.open(); } @@ -201,13 +205,16 @@ const DefaultLiveQueryController = { subscription.on('error', (object) => { subscriptionWrap.emit('error', object); }); + + return Promise.resolve(subscriptionWrap); }); }); - return subscriptionWrap; }, unsubscribe(subscription: any) { - getLiveQueryClient().then((liveQueryClient) => { - return liveQueryClient.unsubscribe(subscription); + return getLiveQueryClient().then((liveQueryClient) => { + return Promise.resolve( + liveQueryClient.unsubscribe(subscription) + ); }); }, _clearCachedDefaultClient() { From f1be5be388d36423f3782718c6cee3c01cee04ac Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 22 Mar 2019 11:56:28 -0500 Subject: [PATCH 2/5] initial test and cleanup --- integration/server.js | 6 +- integration/test/ParseLiveQueryTest.js | 119 +++++++++++++++++++++++++ integration/test/helper.js | 7 +- src/LiveQuerySubscription.js | 1 - src/ParseLiveQuery.js | 18 ++-- src/ParseQuery.js | 2 +- src/__tests__/ParseLiveQuery-test.js | 4 +- 7 files changed, 139 insertions(+), 18 deletions(-) create mode 100644 integration/test/ParseLiveQueryTest.js diff --git a/integration/server.js b/integration/server.js index 436295af6..03efd23af 100644 --- a/integration/server.js +++ b/integration/server.js @@ -9,7 +9,11 @@ const api = new ParseServer({ appId: 'integration', masterKey: 'notsosecret', serverURL: 'http://localhost:1337/parse', // Don't forget to change to https if needed - cloud: `${__dirname}/cloud/main.js` + cloud: `${__dirname}/cloud/main.js`, + liveQuery: { + classNames: ['TestObject', 'DiffObject'], + }, + startLiveQueryServer: true, }); // Serve the Parse API on the /parse URL prefix diff --git a/integration/test/ParseLiveQueryTest.js b/integration/test/ParseLiveQueryTest.js new file mode 100644 index 000000000..acd1c9769 --- /dev/null +++ b/integration/test/ParseLiveQueryTest.js @@ -0,0 +1,119 @@ +'use strict'; + +const assert = require('assert'); +const clear = require('./clear'); +const Parse = require('../../node'); + +const TestObject = Parse.Object.extend('TestObject'); +const DiffObject = Parse.Object.extend('DiffObject'); + +describe('Parse LiveQuery', () => { + beforeEach((done) => { + Parse.initialize('integration'); + Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse'); + Parse.Storage._clear(); + clear().then(done).catch(done.fail); + }); + + it('can subscribe to query', async () => { + const object = new TestObject(); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + + subscription.on('update', object => { + assert.equal(object.get('foo'), 'bar'); + }) + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('can subscribe to multiple queries', async (done) => { + const objectA = new TestObject(); + const objectB = new TestObject(); + await Parse.Object.saveAll([objectA, objectB]); + + const queryA = new Parse.Query(TestObject); + const queryB = new Parse.Query(TestObject); + queryA.equalTo('objectId', objectA.id); + queryB.equalTo('objectId', objectB.id); + const subscriptionA = await queryA.subscribe(); + const subscriptionB = await queryB.subscribe(); + let count = 0; + subscriptionA.on('update', object => { + count++; + assert.equal(object.get('foo'), 'bar'); + }) + subscriptionB.on('update', object => { + count++; + assert.equal(object.get('foo'), 'baz'); + }) + await objectA.save({ foo: 'bar' }); + await objectB.save({ foo: 'baz' }); + + setTimeout(() => { + assert.equal(count, 2); + done(); + }, 100); + }); + + it('can subscribe to multiple queries different class', async (done) => { + const objectA = new TestObject(); + const objectB = new DiffObject(); + await Parse.Object.saveAll([objectA, objectB]); + + const queryA = new Parse.Query(TestObject); + const queryB = new Parse.Query(DiffObject); + queryA.equalTo('objectId', objectA.id); + queryB.equalTo('objectId', objectB.id); + const subscriptionA = await queryA.subscribe(); + const subscriptionB = await queryB.subscribe(); + let count = 0; + subscriptionA.on('update', object => { + count++; + assert.equal(object.get('foo'), 'bar'); + }) + subscriptionB.on('update', object => { + count++; + assert.equal(object.get('foo'), 'baz'); + }) + await objectA.save({ foo: 'bar' }); + await objectB.save({ foo: 'baz' }); + + setTimeout(() => { + expect(count).toBe(2); + done(); + }, 1000); + }); + + it('can unsubscribe to multiple queries different class', async (done) => { + const objectA = new TestObject(); + const objectB = new DiffObject(); + await Parse.Object.saveAll([objectA, objectB]); + + const queryA = new Parse.Query(TestObject); + const queryB = new Parse.Query(DiffObject); + queryA.equalTo('objectId', objectA.id); + queryB.equalTo('objectId', objectB.id); + const subscriptionA = await queryA.subscribe(); + const subscriptionB = await queryB.subscribe(); + let count = 0; + subscriptionA.on('update', () => { + count++; + }) + subscriptionB.on('update', object => { + count++; + assert.equal(object.get('foo'), 'baz'); + }) + subscriptionA.unsubscribe(); + await objectA.save({ foo: 'bar' }); + await objectB.save({ foo: 'baz' }); + + setTimeout(() => { + assert.equal(count, 1); + done(); + }, 1000); + }); +}); diff --git a/integration/test/helper.js b/integration/test/helper.js index c1cb726cc..df4355669 100644 --- a/integration/test/helper.js +++ b/integration/test/helper.js @@ -1,8 +1,13 @@ +const ParseServer = require('parse-server').ParseServer; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; beforeAll((done) => { const { app } = require('../server'); - app.listen(1337, () => { + const httpServer = require('http').createServer(app); + + httpServer.listen(1337, () => { console.log('parse-server running on port 1337.'); done(); }); + ParseServer.createLiveQueryServer(httpServer); }); diff --git a/src/LiveQuerySubscription.js b/src/LiveQuerySubscription.js index 16a81b6b6..b2013a26b 100644 --- a/src/LiveQuerySubscription.js +++ b/src/LiveQuerySubscription.js @@ -110,7 +110,6 @@ class Subscription extends EventEmitter { return CoreManager.getLiveQueryController().getDefaultLiveQueryClient().then((liveQueryClient) => { liveQueryClient.unsubscribe(this); this.emit('close'); - return Promise.resolve(this); }); } } diff --git a/src/ParseLiveQuery.js b/src/ParseLiveQuery.js index 20eaf4619..0c3ff219f 100644 --- a/src/ParseLiveQuery.js +++ b/src/ParseLiveQuery.js @@ -150,19 +150,15 @@ const DefaultLiveQueryController = { }, open() { return getLiveQueryClient().then((liveQueryClient) => { - return Promise.resolve( - liveQueryClient.open() - ); + return liveQueryClient.open(); }); }, close() { return getLiveQueryClient().then((liveQueryClient) => { - return Promise.resolve( - liveQueryClient.close() - ); + return liveQueryClient.close(); }); }, - subscribe(query: any): EventEmitter { + subscribe(query: any): Promise { const subscriptionWrap = new EventEmitter(); return getLiveQueryClient().then((liveQueryClient) => { @@ -180,7 +176,7 @@ const DefaultLiveQueryController = { subscriptionWrap.query = subscription.query; subscriptionWrap.sessionToken = subscription.sessionToken; subscriptionWrap.unsubscribe = subscription.unsubscribe; - // Cannot create these events on a nested way because of EventEmiiter from React Native + // Cannot create these events on a nested way because of EventEmitter from React Native subscription.on('open', () => { subscriptionWrap.emit('open'); }); @@ -206,15 +202,13 @@ const DefaultLiveQueryController = { subscriptionWrap.emit('error', object); }); - return Promise.resolve(subscriptionWrap); + return subscriptionWrap; }); }); }, unsubscribe(subscription: any) { return getLiveQueryClient().then((liveQueryClient) => { - return Promise.resolve( - liveQueryClient.unsubscribe(subscription) - ); + return liveQueryClient.unsubscribe(subscription); }); }, _clearCachedDefaultClient() { diff --git a/src/ParseQuery.js b/src/ParseQuery.js index e0ea9cc66..a8d7408a9 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -1481,7 +1481,7 @@ class ParseQuery { * @return {LiveQuerySubscription} Returns the liveQuerySubscription, it's an event emitter * which can be used to get liveQuery updates. */ - subscribe(): any { + subscribe(): Promise { const controller = CoreManager.getLiveQueryController(); return controller.subscribe(this); } diff --git a/src/__tests__/ParseLiveQuery-test.js b/src/__tests__/ParseLiveQuery-test.js index 497962575..adf463e91 100644 --- a/src/__tests__/ParseLiveQuery-test.js +++ b/src/__tests__/ParseLiveQuery-test.js @@ -122,11 +122,11 @@ describe('ParseLiveQuery', () => { const controller = CoreManager.getLiveQueryController(); - controller.getDefaultLiveQueryClient().then((client) => { + controller.getDefaultLiveQueryClient().then(async (client) => { const query = new ParseQuery("ObjectType"); query.equalTo("test", "value"); - const ourSubscription = controller.subscribe(query, "close"); + const ourSubscription = await controller.subscribe(query, "close"); const isCalled = {}; ["open", From 2e39c96b216f69b3f78de39630d49ddd8ae975e6 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 22 Mar 2019 13:39:58 -0500 Subject: [PATCH 3/5] Massive cleanup --- src/CoreManager.js | 7 +- src/LiveQueryClient.js | 4 - src/LiveQuerySubscription.js | 15 ++- src/ParseLiveQuery.js | 185 ++++++++------------------- src/ParseQuery.js | 17 ++- src/__tests__/ParseLiveQuery-test.js | 2 +- 6 files changed, 78 insertions(+), 152 deletions(-) diff --git a/src/CoreManager.js b/src/CoreManager.js index af7d97d6f..055f85ef5 100644 --- a/src/CoreManager.js +++ b/src/CoreManager.js @@ -393,10 +393,9 @@ module.exports = { setLiveQueryController(controller: any) { requireMethods('LiveQueryController', [ - 'subscribe', - 'unsubscribe', - 'open', - 'close', + 'setDefaultLiveQueryClient', + 'getDefaultLiveQueryClient', + '_clearCachedDefaultClient', ], controller); config['LiveQueryController'] = controller; }, diff --git a/src/LiveQueryClient.js b/src/LiveQueryClient.js index c9fdea898..7aa39bc21 100644 --- a/src/LiveQueryClient.js +++ b/src/LiveQueryClient.js @@ -209,10 +209,6 @@ class LiveQueryClient extends EventEmitter { this.socket.send(JSON.stringify(subscribeRequest)); }); - // adding listener so process does not crash - // best practice is for developer to register their own listener - subscription.on('error', () => {}); - return subscription; } diff --git a/src/LiveQuerySubscription.js b/src/LiveQuerySubscription.js index b2013a26b..c65d2d2ff 100644 --- a/src/LiveQuerySubscription.js +++ b/src/LiveQuerySubscription.js @@ -101,16 +101,19 @@ class Subscription extends EventEmitter { this.id = id; this.query = query; this.sessionToken = sessionToken; + + // adding listener so process does not crash + // best practice is for developer to register their own listener + this.on('error', () => {}); } /** - * closes the subscription + * Closes the subscription */ - unsubscribe() { - return CoreManager.getLiveQueryController().getDefaultLiveQueryClient().then((liveQueryClient) => { - liveQueryClient.unsubscribe(this); - this.emit('close'); - }); + async unsubscribe(): Promise { + const liveQueryClient = await CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + liveQueryClient.unsubscribe(this); + this.emit('close'); } } diff --git a/src/ParseLiveQuery.js b/src/ParseLiveQuery.js index 0c3ff219f..30dab0522 100644 --- a/src/ParseLiveQuery.js +++ b/src/ParseLiveQuery.js @@ -13,14 +13,8 @@ import EventEmitter from './EventEmitter'; import LiveQueryClient from './LiveQueryClient'; import CoreManager from './CoreManager'; -function open() { - const LiveQueryController = CoreManager.getLiveQueryController(); - return LiveQueryController.open(); -} - -function close() { - const LiveQueryController = CoreManager.getLiveQueryController(); - return LiveQueryController.close(); +function getLiveQueryClient(): LiveQueryClient { + return CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); } /** @@ -57,10 +51,11 @@ const LiveQuery = new EventEmitter(); /** * After open is called, the LiveQuery will try to send a connect request * to the LiveQuery server. - * - */ -LiveQuery.open = open; +LiveQuery.open = async () => { + const liveQueryClient = await getLiveQueryClient(); + return liveQueryClient.open(); +}; /** * When you're done using LiveQuery, you can call Parse.LiveQuery.close(). @@ -68,148 +63,72 @@ LiveQuery.open = open; * cancel the auto reconnect, and unsubscribe all subscriptions based on it. * If you call query.subscribe() after this, we'll create a new WebSocket * connection to the LiveQuery server. - * - */ -LiveQuery.close = close; +LiveQuery.close = async () => { + const liveQueryClient = await getLiveQueryClient(); + return liveQueryClient.close(); +}; + // Register a default onError callback to make sure we do not crash on error -LiveQuery.on('error', () => { -}); +LiveQuery.on('error', () => {}); export default LiveQuery; -function getSessionToken() { - const controller = CoreManager.getUserController(); - return controller.currentUserAsync().then((currentUser) => { - return currentUser ? currentUser.getSessionToken() : undefined; - }); -} - -function getLiveQueryClient() { - return CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); -} - let defaultLiveQueryClient; + const DefaultLiveQueryController = { - setDefaultLiveQueryClient(liveQueryClient: any) { + setDefaultLiveQueryClient(liveQueryClient: LiveQueryClient) { defaultLiveQueryClient = liveQueryClient; }, - getDefaultLiveQueryClient(): Promise { + + async getDefaultLiveQueryClient(): Promise { if (defaultLiveQueryClient) { - return Promise.resolve(defaultLiveQueryClient); + return defaultLiveQueryClient; + } + const currentUser = await CoreManager.getUserController().currentUserAsync(); + const sessionToken = currentUser ? currentUser.getSessionToken() : undefined; + + let liveQueryServerURL = CoreManager.get('LIVEQUERY_SERVER_URL'); + if (liveQueryServerURL && liveQueryServerURL.indexOf('ws') !== 0) { + throw new Error( + 'You need to set a proper Parse LiveQuery server url before using LiveQueryClient' + ); } - return getSessionToken().then((sessionToken) => { - let liveQueryServerURL = CoreManager.get('LIVEQUERY_SERVER_URL'); - - if (liveQueryServerURL && liveQueryServerURL.indexOf('ws') !== 0) { - throw new Error( - 'You need to set a proper Parse LiveQuery server url before using LiveQueryClient' - ); - } - - // If we can not find Parse.liveQueryServerURL, we try to extract it from Parse.serverURL - if (!liveQueryServerURL) { - const tempServerURL = CoreManager.get('SERVER_URL'); - let protocol = 'ws://'; - // If Parse is being served over SSL/HTTPS, ensure LiveQuery Server uses 'wss://' prefix - if (tempServerURL.indexOf('https') === 0) { - protocol = 'wss://' - } - const host = tempServerURL.replace(/^https?:\/\//, ''); - liveQueryServerURL = protocol + host; - CoreManager.set('LIVEQUERY_SERVER_URL', liveQueryServerURL); + // If we can not find Parse.liveQueryServerURL, we try to extract it from Parse.serverURL + if (!liveQueryServerURL) { + const tempServerURL = CoreManager.get('SERVER_URL'); + let protocol = 'ws://'; + if (tempServerURL.indexOf('https') === 0) { + protocol = 'wss://' } + const host = tempServerURL.replace(/^https?:\/\//, ''); + liveQueryServerURL = protocol + host; + CoreManager.set('LIVEQUERY_SERVER_URL', liveQueryServerURL); + } - const applicationId = CoreManager.get('APPLICATION_ID'); - const javascriptKey = CoreManager.get('JAVASCRIPT_KEY'); - const masterKey = CoreManager.get('MASTER_KEY'); - // Get currentUser sessionToken if possible - defaultLiveQueryClient = new LiveQueryClient({ - applicationId, - serverURL: liveQueryServerURL, - javascriptKey, - masterKey, - sessionToken, - }); - // Register a default onError callback to make sure we do not crash on error - // Cannot create these events on a nested way because of EventEmiiter from React Native - defaultLiveQueryClient.on('error', (error) => { - LiveQuery.emit('error', error); - }); - defaultLiveQueryClient.on('open', () => { - LiveQuery.emit('open'); - }); - defaultLiveQueryClient.on('close', () => { - LiveQuery.emit('close'); - }); + const applicationId = CoreManager.get('APPLICATION_ID'); + const javascriptKey = CoreManager.get('JAVASCRIPT_KEY'); + const masterKey = CoreManager.get('MASTER_KEY'); - return defaultLiveQueryClient; + defaultLiveQueryClient = new LiveQueryClient({ + applicationId, + serverURL: liveQueryServerURL, + javascriptKey, + masterKey, + sessionToken, }); - }, - open() { - return getLiveQueryClient().then((liveQueryClient) => { - return liveQueryClient.open(); + defaultLiveQueryClient.on('error', (error) => { + LiveQuery.emit('error', error); }); - }, - close() { - return getLiveQueryClient().then((liveQueryClient) => { - return liveQueryClient.close(); + defaultLiveQueryClient.on('open', () => { + LiveQuery.emit('open'); }); - }, - subscribe(query: any): Promise { - const subscriptionWrap = new EventEmitter(); - - return getLiveQueryClient().then((liveQueryClient) => { - if (liveQueryClient.shouldOpen()) { - liveQueryClient.open(); - } - const promiseSessionToken = getSessionToken(); - // new event emitter - return promiseSessionToken.then((sessionToken) => { - - const subscription = liveQueryClient.subscribe(query, sessionToken); - // enter, leave create, etc - - subscriptionWrap.id = subscription.id; - subscriptionWrap.query = subscription.query; - subscriptionWrap.sessionToken = subscription.sessionToken; - subscriptionWrap.unsubscribe = subscription.unsubscribe; - // Cannot create these events on a nested way because of EventEmitter from React Native - subscription.on('open', () => { - subscriptionWrap.emit('open'); - }); - subscription.on('create', (object) => { - subscriptionWrap.emit('create', object); - }); - subscription.on('update', (object) => { - subscriptionWrap.emit('update', object); - }); - subscription.on('enter', (object) => { - subscriptionWrap.emit('enter', object); - }); - subscription.on('leave', (object) => { - subscriptionWrap.emit('leave', object); - }); - subscription.on('delete', (object) => { - subscriptionWrap.emit('delete', object); - }); - subscription.on('close', (object) => { - subscriptionWrap.emit('close', object); - }); - subscription.on('error', (object) => { - subscriptionWrap.emit('error', object); - }); - - return subscriptionWrap; - }); - }); - }, - unsubscribe(subscription: any) { - return getLiveQueryClient().then((liveQueryClient) => { - return liveQueryClient.unsubscribe(subscription); + defaultLiveQueryClient.on('close', () => { + LiveQuery.emit('close'); }); + return defaultLiveQueryClient; }, _clearCachedDefaultClient() { defaultLiveQueryClient = null; diff --git a/src/ParseQuery.js b/src/ParseQuery.js index a8d7408a9..2a3d4003d 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -17,6 +17,7 @@ import ParseGeoPoint from './ParseGeoPoint'; import ParseObject from './ParseObject'; import OfflineQuery from './OfflineQuery'; +import type LiveQuerySubscription from './LiveQuerySubscription'; import type { RequestOptions, FullOptions } from './RESTController'; type BatchOptions = FullOptions & { batchSize?: number }; @@ -1478,12 +1479,20 @@ class ParseQuery { /** * Subscribe this query to get liveQuery updates - * @return {LiveQuerySubscription} Returns the liveQuerySubscription, it's an event emitter + * + * @return {Promise} Returns the liveQuerySubscription, it's an event emitter * which can be used to get liveQuery updates. */ - subscribe(): Promise { - const controller = CoreManager.getLiveQueryController(); - return controller.subscribe(this); + async subscribe(): Promise { + const currentUser = await CoreManager.getUserController().currentUserAsync(); + const sessionToken = currentUser ? currentUser.getSessionToken() : undefined; + + const liveQueryClient = await CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + if (liveQueryClient.shouldOpen()) { + liveQueryClient.open(); + } + const subscription = liveQueryClient.subscribe(this, sessionToken); + return subscription; } /** diff --git a/src/__tests__/ParseLiveQuery-test.js b/src/__tests__/ParseLiveQuery-test.js index adf463e91..1e2550a5e 100644 --- a/src/__tests__/ParseLiveQuery-test.js +++ b/src/__tests__/ParseLiveQuery-test.js @@ -126,7 +126,7 @@ describe('ParseLiveQuery', () => { const query = new ParseQuery("ObjectType"); query.equalTo("test", "value"); - const ourSubscription = await controller.subscribe(query, "close"); + const ourSubscription = await client.subscribe(query, "close"); const isCalled = {}; ["open", From 8b1c9e310d5d83b80887c77b2596af44b1d72f0d Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 22 Mar 2019 14:49:58 -0500 Subject: [PATCH 4/5] improve coverage --- src/__tests__/ParseLiveQuery-test.js | 83 +++++++++++++++++++++++++- src/__tests__/ParseQuery-test.js | 89 ++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 2 deletions(-) diff --git a/src/__tests__/ParseLiveQuery-test.js b/src/__tests__/ParseLiveQuery-test.js index 1e2550a5e..35e98b4fe 100644 --- a/src/__tests__/ParseLiveQuery-test.js +++ b/src/__tests__/ParseLiveQuery-test.js @@ -17,10 +17,14 @@ jest.dontMock('../EventEmitter'); jest.dontMock('../promiseUtils'); // Forces the loading -require('../ParseLiveQuery'); +const LiveQuery = require('../ParseLiveQuery').default; const CoreManager = require('../CoreManager'); const ParseQuery = require('../ParseQuery').default; const LiveQuerySubscription = require('../LiveQuerySubscription').default; +const mockLiveQueryClient = { + open: jest.fn(), + close: jest.fn(), +}; describe('ParseLiveQuery', () => { beforeEach(() => { @@ -63,7 +67,7 @@ describe('ParseLiveQuery', () => { }); }); - it('automatically generates a websocket url', (done) => { + it('automatically generates a ws websocket url', (done) => { CoreManager.set('UserController', { currentUserAsync() { return Promise.resolve(undefined); @@ -71,6 +75,27 @@ describe('ParseLiveQuery', () => { }); CoreManager.set('APPLICATION_ID', 'appid'); CoreManager.set('JAVASCRIPT_KEY', 'jskey'); + CoreManager.set('SERVER_URL', 'http://api.parse.com/1'); + CoreManager.set('LIVEQUERY_SERVER_URL', null); + const controller = CoreManager.getLiveQueryController(); + controller.getDefaultLiveQueryClient().then((client) => { + expect(client.serverURL).toBe('ws://api.parse.com/1'); + expect(client.applicationId).toBe('appid'); + expect(client.javascriptKey).toBe('jskey'); + expect(client.sessionToken).toBe(undefined); + done(); + }); + }); + + it('automatically generates a wss websocket url', (done) => { + CoreManager.set('UserController', { + currentUserAsync() { + return Promise.resolve(undefined); + } + }); + CoreManager.set('APPLICATION_ID', 'appid'); + CoreManager.set('JAVASCRIPT_KEY', 'jskey'); + CoreManager.set('SERVER_URL', 'https://api.parse.com/1'); CoreManager.set('LIVEQUERY_SERVER_URL', null); const controller = CoreManager.getLiveQueryController(); controller.getDefaultLiveQueryClient().then((client) => { @@ -105,6 +130,32 @@ describe('ParseLiveQuery', () => { }); }); + it('handle LiveQueryClient events', async () => { + const spy = jest.spyOn(LiveQuery, 'emit'); + + CoreManager.set('UserController', { + currentUserAsync() { + return Promise.resolve({ + getSessionToken() { + return 'token'; + } + }); + } + }); + CoreManager.set('APPLICATION_ID', 'appid'); + CoreManager.set('JAVASCRIPT_KEY', 'jskey'); + CoreManager.set('LIVEQUERY_SERVER_URL', null); + const controller = CoreManager.getLiveQueryController(); + const client = await controller.getDefaultLiveQueryClient(); + client.emit('error', 'error thrown'); + client.emit('open'); + client.emit('close'); + expect(spy.mock.calls[0]).toEqual(['error', 'error thrown']); + expect(spy.mock.calls[1]).toEqual(['open']); + expect(spy.mock.calls[2]).toEqual(['close']); + spy.mockRestore(); + }); + it('subscribes to all subscription events', (done) => { CoreManager.set('UserController', { @@ -190,4 +241,32 @@ describe('ParseLiveQuery', () => { const subscription = new LiveQuerySubscription('0', query, 'token'); subscription.unsubscribe().then(done).catch(done.fail); }); + + it('can handle LiveQuery open event', async () => { + jest.spyOn(mockLiveQueryClient, 'open'); + const controller = CoreManager.getLiveQueryController(); + controller.setDefaultLiveQueryClient(mockLiveQueryClient); + + await LiveQuery.open(); + expect(mockLiveQueryClient.open).toHaveBeenCalled(); + }); + + it('can handle LiveQuery close event', async () => { + jest.spyOn(mockLiveQueryClient, 'close'); + const controller = CoreManager.getLiveQueryController(); + controller.setDefaultLiveQueryClient(mockLiveQueryClient); + + await LiveQuery.close(); + expect(mockLiveQueryClient.close).toHaveBeenCalled(); + }); + + it('can handle LiveQuery error event', async () => { + try { + LiveQuery.emit('error'); + expect(true).toBe(true); + } catch (error) { + // Should not throw error + expect(false).toBe(true); + } + }); }); diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 347bd8755..13896db19 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -19,6 +19,7 @@ jest.dontMock('../UniqueInstanceStateController'); jest.dontMock('../ObjectStateMutations'); jest.dontMock('../LocalDatastore'); jest.dontMock('../OfflineQuery'); +jest.dontMock('../LiveQuerySubscription'); const mockObject = function(className) { this.className = className; @@ -49,6 +50,7 @@ const ParseError = require('../ParseError').default; const ParseGeoPoint = require('../ParseGeoPoint').default; let ParseObject = require('../ParseObject'); let ParseQuery = require('../ParseQuery').default; +const LiveQuerySubscription = require('../LiveQuerySubscription').default; describe('ParseQuery', () => { it('can be constructed from a class name', () => { @@ -2627,4 +2629,91 @@ describe('ParseQuery LocalDatastore', () => { const results = await q.find(); expect(results[0].get('foo')).toEqual('baz'); }); + + it ('can subscribe to query if client is already open', async () => { + const mockLiveQueryClient = { + shouldOpen: function() { + return false; + }, + subscribe: function(query, sessionToken) { + return new LiveQuerySubscription('0', query, sessionToken); + }, + }; + CoreManager.set('UserController', { + currentUserAsync() { + return Promise.resolve({ + getSessionToken() { + return 'token'; + } + }); + } + }); + CoreManager.set('LiveQueryController', { + getDefaultLiveQueryClient() { + return Promise.resolve(mockLiveQueryClient); + } + }); + const query = new ParseQuery('TestObject'); + const subscription = await query.subscribe(); + expect(subscription.id).toBe('0'); + expect(subscription.sessionToken).toBe('token'); + expect(subscription.query).toEqual(query); + }); + + it ('can subscribe to query if client is not open', async () => { + const mockLiveQueryClient = { + shouldOpen: function() { + return true; + }, + open: function() {}, + subscribe: function(query, sessionToken) { + return new LiveQuerySubscription('0', query, sessionToken); + }, + }; + CoreManager.set('UserController', { + currentUserAsync() { + return Promise.resolve({ + getSessionToken() { + return 'token'; + } + }); + } + }); + CoreManager.set('LiveQueryController', { + getDefaultLiveQueryClient() { + return Promise.resolve(mockLiveQueryClient); + } + }); + const query = new ParseQuery('TestObject'); + const subscription = await query.subscribe(); + expect(subscription.id).toBe('0'); + expect(subscription.sessionToken).toBe('token'); + expect(subscription.query).toEqual(query); + }); + it ('can subscribe to query without sessionToken', async () => { + const mockLiveQueryClient = { + shouldOpen: function() { + return true; + }, + open: function() {}, + subscribe: function(query, sessionToken) { + return new LiveQuerySubscription('0', query, sessionToken); + }, + }; + CoreManager.set('UserController', { + currentUserAsync() { + return Promise.resolve(null); + } + }); + CoreManager.set('LiveQueryController', { + getDefaultLiveQueryClient() { + return Promise.resolve(mockLiveQueryClient); + } + }); + const query = new ParseQuery('TestObject'); + const subscription = await query.subscribe(); + expect(subscription.id).toBe('0'); + expect(subscription.sessionToken).toBeUndefined(); + expect(subscription.query).toEqual(query); + }); }); From 542f128d448ca4ba31cdef18bf311f661d30833c Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 22 Mar 2019 17:07:21 -0500 Subject: [PATCH 5/5] quick nits --- integration/test/ParseLiveQueryTest.js | 29 ++++++++++++++++++++++++++ src/LiveQuerySubscription.js | 11 +++++----- src/ParseLiveQuery.js | 11 ++++------ src/__tests__/ParseLiveQuery-test.js | 2 +- src/__tests__/ParseQuery-test.js | 7 ++++--- 5 files changed, 44 insertions(+), 16 deletions(-) diff --git a/integration/test/ParseLiveQueryTest.js b/integration/test/ParseLiveQueryTest.js index acd1c9769..535a73fe4 100644 --- a/integration/test/ParseLiveQueryTest.js +++ b/integration/test/ParseLiveQueryTest.js @@ -116,4 +116,33 @@ describe('Parse LiveQuery', () => { done(); }, 1000); }); + + it('can unsubscribe with await to multiple queries different class', async (done) => { + const objectA = new TestObject(); + const objectB = new DiffObject(); + await Parse.Object.saveAll([objectA, objectB]); + + const queryA = new Parse.Query(TestObject); + const queryB = new Parse.Query(DiffObject); + queryA.equalTo('objectId', objectA.id); + queryB.equalTo('objectId', objectB.id); + const subscriptionA = await queryA.subscribe(); + const subscriptionB = await queryB.subscribe(); + let count = 0; + subscriptionA.on('update', () => { + count++; + }) + subscriptionB.on('update', object => { + count++; + assert.equal(object.get('foo'), 'baz'); + }) + await subscriptionA.unsubscribe(); + await objectA.save({ foo: 'bar' }); + await objectB.save({ foo: 'baz' }); + + setTimeout(() => { + assert.equal(count, 1); + done(); + }, 1000); + }); }); diff --git a/src/LiveQuerySubscription.js b/src/LiveQuerySubscription.js index c65d2d2ff..8ea25d751 100644 --- a/src/LiveQuerySubscription.js +++ b/src/LiveQuerySubscription.js @@ -108,12 +108,13 @@ class Subscription extends EventEmitter { } /** - * Closes the subscription + * Close the subscription */ - async unsubscribe(): Promise { - const liveQueryClient = await CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); - liveQueryClient.unsubscribe(this); - this.emit('close'); + unsubscribe(): Promise { + return CoreManager.getLiveQueryController().getDefaultLiveQueryClient().then((liveQueryClient) => { + liveQueryClient.unsubscribe(this); + this.emit('close'); + }); } } diff --git a/src/ParseLiveQuery.js b/src/ParseLiveQuery.js index 30dab0522..ea775ec5a 100644 --- a/src/ParseLiveQuery.js +++ b/src/ParseLiveQuery.js @@ -12,6 +12,7 @@ import EventEmitter from './EventEmitter'; import LiveQueryClient from './LiveQueryClient'; import CoreManager from './CoreManager'; +const url = require('url'); function getLiveQueryClient(): LiveQueryClient { return CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); @@ -98,13 +99,9 @@ const DefaultLiveQueryController = { // If we can not find Parse.liveQueryServerURL, we try to extract it from Parse.serverURL if (!liveQueryServerURL) { - const tempServerURL = CoreManager.get('SERVER_URL'); - let protocol = 'ws://'; - if (tempServerURL.indexOf('https') === 0) { - protocol = 'wss://' - } - const host = tempServerURL.replace(/^https?:\/\//, ''); - liveQueryServerURL = protocol + host; + const serverURL = url.parse(CoreManager.get('SERVER_URL')); + const protocol = serverURL.protocol === 'http:' ? 'ws://' : 'wss://'; + liveQueryServerURL = protocol + serverURL.host + serverURL.pathname; CoreManager.set('LIVEQUERY_SERVER_URL', liveQueryServerURL); } diff --git a/src/__tests__/ParseLiveQuery-test.js b/src/__tests__/ParseLiveQuery-test.js index 35e98b4fe..22b0cac2b 100644 --- a/src/__tests__/ParseLiveQuery-test.js +++ b/src/__tests__/ParseLiveQuery-test.js @@ -193,7 +193,7 @@ describe('ParseLiveQuery', () => { }); }); - // controller.subscribe() completes asynchronously, + // client.subscribe() completes asynchronously, // so we need to give it a chance to complete before finishing setTimeout(() => { try { diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 13896db19..194a38402 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -2630,7 +2630,7 @@ describe('ParseQuery LocalDatastore', () => { expect(results[0].get('foo')).toEqual('baz'); }); - it ('can subscribe to query if client is already open', async () => { + it('can subscribe to query if client is already open', async () => { const mockLiveQueryClient = { shouldOpen: function() { return false; @@ -2660,7 +2660,7 @@ describe('ParseQuery LocalDatastore', () => { expect(subscription.query).toEqual(query); }); - it ('can subscribe to query if client is not open', async () => { + it('can subscribe to query if client is not open', async () => { const mockLiveQueryClient = { shouldOpen: function() { return true; @@ -2690,7 +2690,8 @@ describe('ParseQuery LocalDatastore', () => { expect(subscription.sessionToken).toBe('token'); expect(subscription.query).toEqual(query); }); - it ('can subscribe to query without sessionToken', async () => { + + it('can subscribe to query without sessionToken', async () => { const mockLiveQueryClient = { shouldOpen: function() { return true;