From fc92faac75107b3392eeddd916c4c5b45e3c5e0c Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 16 Jan 2023 22:32:22 +1100 Subject: [PATCH] feat: Add `ParseQuery.watch` to trigger LiveQuery only on update of specific fields (#8028) --- spec/ParseLiveQuery.spec.js | 43 +++++++++++++++++++++ spec/ParseLiveQueryServer.spec.js | 55 +++++++++++++++++++++++++++ src/LiveQuery/ParseLiveQueryServer.js | 19 +++++++++ 3 files changed, 117 insertions(+) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index e2a5f68d3b..fbac779ce8 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -394,6 +394,49 @@ describe('ParseLiveQuery', function () { await object.save(); }); + xit('can handle live query with fields - enable upon JS SDK support', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['Test'], + }, + startLiveQueryServer: true, + }); + const query = new Parse.Query('Test'); + query.watch('yolo'); + const subscription = await query.subscribe(); + const spy = { + create(obj) { + if (!obj.get('yolo')) { + fail('create should not have been called'); + } + }, + update(object, original) { + if (object.get('yolo') === original.get('yolo')) { + fail('create should not have been called'); + } + }, + }; + const createSpy = spyOn(spy, 'create').and.callThrough(); + const updateSpy = spyOn(spy, 'update').and.callThrough(); + subscription.on('create', spy.create); + subscription.on('update', spy.update); + const obj = new Parse.Object('Test'); + obj.set('foo', 'bar'); + await obj.save(); + obj.set('foo', 'xyz'); + obj.set('yolo', 'xyz'); + await obj.save(); + const obj2 = new Parse.Object('Test'); + obj2.set('foo', 'bar'); + obj2.set('yolo', 'bar'); + await obj2.save(); + obj2.set('foo', 'bart'); + await obj2.save(); + await new Promise(resolve => setTimeout(resolve, 2000)); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(updateSpy).toHaveBeenCalledTimes(1); + }); + it('can handle afterEvent set pointers', async done => { await reconfigureServer({ liveQuery: { diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index a7ef274ae6..47e90733a1 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -1087,6 +1087,61 @@ describe('ParseLiveQueryServer', function () { done(); }); + it('can handle create command with watch', async () => { + jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client'); + const Client = require('../lib/LiveQuery/Client').Client; + const parseLiveQueryServer = new ParseLiveQueryServer({}); + // Make mock request message + const message = generateMockMessage(); + + const clientId = 1; + const parseWebSocket = { + clientId, + send: jasmine.createSpy('send'), + }; + const client = new Client(clientId, parseWebSocket); + spyOn(client, 'pushCreate').and.callThrough(); + parseLiveQueryServer.clients.set(clientId, client); + + // Add mock subscription + const requestId = 2; + const query = { + className: testClassName, + where: { + key: 'value', + }, + watch: ['yolo'], + }; + await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query); + // Mock _matchesSubscription to return matching + parseLiveQueryServer._matchesSubscription = function (parseObject) { + if (!parseObject) { + return false; + } + return true; + }; + parseLiveQueryServer._matchesACL = function () { + return Promise.resolve(true); + }; + + parseLiveQueryServer._onAfterSave(message); + + // Make sure we send create command to client + await timeout(); + + expect(client.pushCreate).not.toHaveBeenCalled(); + + message.currentParseObject.set('yolo', 'test'); + parseLiveQueryServer._onAfterSave(message); + + await timeout(); + + const args = parseWebSocket.send.calls.mostRecent().args; + const toSend = JSON.parse(args[0]); + expect(toSend.object).toBeDefined(); + expect(toSend.original).toBeUndefined(); + }); + it('can match subscription for null or undefined parse object', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index f33382f202..1ecbda9372 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -22,6 +22,7 @@ import { getCacheController, getDatabaseController } from '../Controllers'; import LRU from 'lru-cache'; import UserRouter from '../Routers/UsersRouter'; import DatabaseController from '../Controllers/DatabaseController'; +import { isDeepStrictEqual } from 'util'; class ParseLiveQueryServer { clients: Map; @@ -329,6 +330,10 @@ class ParseLiveQueryServer { } else { return null; } + const watchFieldsChanged = this._checkWatchFields(client, requestId, message); + if (!watchFieldsChanged && (type === 'update' || type === 'create')) { + return; + } res = { event: type, sessionToken: client.sessionToken, @@ -707,6 +712,17 @@ class ParseLiveQueryServer { return auth; } + _checkWatchFields(client: any, requestId: any, message: any) { + const subscriptionInfo = client.getSubscriptionInfo(requestId); + const watch = subscriptionInfo?.watch; + if (!watch) { + return true; + } + const object = message.currentParseObject; + const original = message.originalParseObject; + return watch.some(field => !isDeepStrictEqual(object.get(field), original?.get(field))); + } + async _matchesACL(acl: any, client: any, requestId: number): Promise { // Return true directly if ACL isn't present, ACL is public read, or client has master key if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) { @@ -888,6 +904,9 @@ class ParseLiveQueryServer { if (request.query.fields) { subscriptionInfo.fields = request.query.fields; } + if (request.query.watch) { + subscriptionInfo.watch = request.query.watch; + } if (request.sessionToken) { subscriptionInfo.sessionToken = request.sessionToken; }