Skip to content

Commit 87dcd23

Browse files
author
Perceval Archimbaud
authored
Fix #7340 by correclty computing function name for push event (#7341)
* Add a failing test for issue #7340 If any delay occurs after "message.event" assignation in LiveQueryServer._onAfterSave, the next subscription or request with a different event might overwrite it, and by that using the wrong "push" function name. * Remove updade of message and use res.event instead This prevent computing function name from a incorrect event if multiple subscriptions override one by one the message.event. * Update CHANGELOG.md * Replace setTimeout by async/await expressions
1 parent 45d00ce commit 87dcd23

File tree

4 files changed

+137
-66
lines changed

4 files changed

+137
-66
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ ___
130130
- Fix file upload issue for S3 compatible storage (Linode, DigitalOcean) by avoiding empty tags property when creating a file (Ali Oguzhan Yildiz) [#7300](https://github.com/parse-community/parse-server/pull/7300)
131131
- Add building Docker image as CI check (Manuel Trezza) [#7332](https://github.com/parse-community/parse-server/pull/7332)
132132
- Add NPM package-lock version check to CI (Manuel Trezza) [#7333](https://github.com/parse-community/parse-server/pull/7333)
133+
- Fix incorrect LiveQuery events triggered for multiple subscriptions on the same class with different events [#7341](https://github.com/parse-community/parse-server/pull/7341)
133134
___
134135
## 4.5.0
135136
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0)

spec/ParseLiveQueryServer.spec.js

+133-63
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const queryHashValue = 'hash';
99
const testUserId = 'userId';
1010
const testClassName = 'TestObject';
1111

12+
const timeout = () => jasmine.timeout(100);
13+
1214
describe('ParseLiveQueryServer', function () {
1315
beforeEach(function (done) {
1416
// Mock ParseWebSocketServer
@@ -750,10 +752,10 @@ describe('ParseLiveQueryServer', function () {
750752

751753
// Make sure we send command to client, since _matchesACL is async, we have to
752754
// wait and check
753-
setTimeout(function () {
754-
expect(client.pushDelete).toHaveBeenCalled();
755-
done();
756-
}, jasmine.ASYNC_TEST_WAIT_TIME);
755+
await timeout();
756+
757+
expect(client.pushDelete).toHaveBeenCalled();
758+
done();
757759
});
758760

759761
it('has no subscription and can handle object save command', async () => {
@@ -785,14 +787,14 @@ describe('ParseLiveQueryServer', function () {
785787
parseLiveQueryServer._onAfterSave(message);
786788

787789
// Make sure we do not send command to client
788-
setTimeout(function () {
789-
expect(client.pushCreate).not.toHaveBeenCalled();
790-
expect(client.pushEnter).not.toHaveBeenCalled();
791-
expect(client.pushUpdate).not.toHaveBeenCalled();
792-
expect(client.pushDelete).not.toHaveBeenCalled();
793-
expect(client.pushLeave).not.toHaveBeenCalled();
794-
done();
795-
}, jasmine.ASYNC_TEST_WAIT_TIME);
790+
await timeout();
791+
792+
expect(client.pushCreate).not.toHaveBeenCalled();
793+
expect(client.pushEnter).not.toHaveBeenCalled();
794+
expect(client.pushUpdate).not.toHaveBeenCalled();
795+
expect(client.pushDelete).not.toHaveBeenCalled();
796+
expect(client.pushLeave).not.toHaveBeenCalled();
797+
done();
796798
});
797799

798800
it('can handle object enter command which matches some subscriptions', async done => {
@@ -822,14 +824,14 @@ describe('ParseLiveQueryServer', function () {
822824
parseLiveQueryServer._onAfterSave(message);
823825

824826
// Make sure we send enter command to client
825-
setTimeout(function () {
826-
expect(client.pushCreate).not.toHaveBeenCalled();
827-
expect(client.pushEnter).toHaveBeenCalled();
828-
expect(client.pushUpdate).not.toHaveBeenCalled();
829-
expect(client.pushDelete).not.toHaveBeenCalled();
830-
expect(client.pushLeave).not.toHaveBeenCalled();
831-
done();
832-
}, jasmine.ASYNC_TEST_WAIT_TIME);
827+
await timeout();
828+
829+
expect(client.pushCreate).not.toHaveBeenCalled();
830+
expect(client.pushEnter).toHaveBeenCalled();
831+
expect(client.pushUpdate).not.toHaveBeenCalled();
832+
expect(client.pushDelete).not.toHaveBeenCalled();
833+
expect(client.pushLeave).not.toHaveBeenCalled();
834+
done();
833835
});
834836

835837
it('can handle object update command which matches some subscriptions', async done => {
@@ -855,14 +857,14 @@ describe('ParseLiveQueryServer', function () {
855857
parseLiveQueryServer._onAfterSave(message);
856858

857859
// Make sure we send update command to client
858-
setTimeout(function () {
859-
expect(client.pushCreate).not.toHaveBeenCalled();
860-
expect(client.pushEnter).not.toHaveBeenCalled();
861-
expect(client.pushUpdate).toHaveBeenCalled();
862-
expect(client.pushDelete).not.toHaveBeenCalled();
863-
expect(client.pushLeave).not.toHaveBeenCalled();
864-
done();
865-
}, jasmine.ASYNC_TEST_WAIT_TIME);
860+
await timeout();
861+
862+
expect(client.pushCreate).not.toHaveBeenCalled();
863+
expect(client.pushEnter).not.toHaveBeenCalled();
864+
expect(client.pushUpdate).toHaveBeenCalled();
865+
expect(client.pushDelete).not.toHaveBeenCalled();
866+
expect(client.pushLeave).not.toHaveBeenCalled();
867+
done();
866868
});
867869

868870
it('can handle object leave command which matches some subscriptions', async done => {
@@ -892,14 +894,81 @@ describe('ParseLiveQueryServer', function () {
892894
parseLiveQueryServer._onAfterSave(message);
893895

894896
// Make sure we send leave command to client
895-
setTimeout(function () {
896-
expect(client.pushCreate).not.toHaveBeenCalled();
897-
expect(client.pushEnter).not.toHaveBeenCalled();
898-
expect(client.pushUpdate).not.toHaveBeenCalled();
899-
expect(client.pushDelete).not.toHaveBeenCalled();
900-
expect(client.pushLeave).toHaveBeenCalled();
901-
done();
902-
}, jasmine.ASYNC_TEST_WAIT_TIME);
897+
await timeout();
898+
899+
expect(client.pushCreate).not.toHaveBeenCalled();
900+
expect(client.pushEnter).not.toHaveBeenCalled();
901+
expect(client.pushUpdate).not.toHaveBeenCalled();
902+
expect(client.pushDelete).not.toHaveBeenCalled();
903+
expect(client.pushLeave).toHaveBeenCalled();
904+
done();
905+
});
906+
907+
it('sends correct events for object with multiple subscriptions', async done => {
908+
const parseLiveQueryServer = new ParseLiveQueryServer({});
909+
910+
Parse.Cloud.afterLiveQueryEvent('TestObject', () => {
911+
// Simulate delay due to trigger, auth, etc.
912+
return jasmine.timeout(10);
913+
});
914+
915+
// Make mock request message
916+
const message = generateMockMessage(true);
917+
// Add mock client
918+
const clientId = 1;
919+
const client = addMockClient(parseLiveQueryServer, clientId);
920+
client.sessionToken = 'sessionToken';
921+
922+
// Mock queryHash for this special test
923+
const mockQueryHash = jasmine.createSpy('matchesQuery').and.returnValue('hash1');
924+
jasmine.mockLibrary('../lib/LiveQuery/QueryTools', 'queryHash', mockQueryHash);
925+
// Add mock subscription 1
926+
const requestId2 = 2;
927+
await addMockSubscription(parseLiveQueryServer, clientId, requestId2, null, null, 'hash1');
928+
929+
// Mock queryHash for this special test
930+
const mockQueryHash2 = jasmine.createSpy('matchesQuery').and.returnValue('hash2');
931+
jasmine.mockLibrary('../lib/LiveQuery/QueryTools', 'queryHash', mockQueryHash2);
932+
// Add mock subscription 2
933+
const requestId3 = 3;
934+
await addMockSubscription(parseLiveQueryServer, clientId, requestId3, null, null, 'hash2');
935+
// Mock _matchesSubscription to return matching
936+
// In order to mimic a leave, then enter, we need original match return true
937+
// and the current match return false, then the other way around
938+
let counter = 0;
939+
parseLiveQueryServer._matchesSubscription = function (parseObject) {
940+
if (!parseObject) {
941+
return false;
942+
}
943+
counter += 1;
944+
// true, false, false, true
945+
return counter < 2 || counter > 3;
946+
};
947+
parseLiveQueryServer._matchesACL = function () {
948+
// Simulate call
949+
return jasmine.timeout(10).then(() => true);
950+
};
951+
parseLiveQueryServer._onAfterSave(message);
952+
953+
// Make sure we send leave and enter command to client
954+
await timeout();
955+
956+
expect(client.pushCreate).not.toHaveBeenCalled();
957+
expect(client.pushEnter).toHaveBeenCalledTimes(1);
958+
expect(client.pushEnter).toHaveBeenCalledWith(
959+
requestId3,
960+
{ key: 'value', className: 'TestObject' },
961+
{ key: 'originalValue', className: 'TestObject' }
962+
);
963+
expect(client.pushUpdate).not.toHaveBeenCalled();
964+
expect(client.pushDelete).not.toHaveBeenCalled();
965+
expect(client.pushLeave).toHaveBeenCalledTimes(1);
966+
expect(client.pushLeave).toHaveBeenCalledWith(
967+
requestId2,
968+
{ key: 'value', className: 'TestObject' },
969+
{ key: 'originalValue', className: 'TestObject' }
970+
);
971+
done();
903972
});
904973

905974
it('can handle update command with original object', async done => {
@@ -936,15 +1005,15 @@ describe('ParseLiveQueryServer', function () {
9361005
parseLiveQueryServer._onAfterSave(message);
9371006

9381007
// Make sure we send update command to client
939-
setTimeout(function () {
940-
expect(client.pushUpdate).toHaveBeenCalled();
941-
const args = parseWebSocket.send.calls.mostRecent().args;
942-
const toSend = JSON.parse(args[0]);
1008+
await timeout();
9431009

944-
expect(toSend.object).toBeDefined();
945-
expect(toSend.original).toBeDefined();
946-
done();
947-
}, jasmine.ASYNC_TEST_WAIT_TIME);
1010+
expect(client.pushUpdate).toHaveBeenCalled();
1011+
const args = parseWebSocket.send.calls.mostRecent().args;
1012+
const toSend = JSON.parse(args[0]);
1013+
1014+
expect(toSend.object).toBeDefined();
1015+
expect(toSend.original).toBeDefined();
1016+
done();
9481017
});
9491018

9501019
it('can handle object create command which matches some subscriptions', async done => {
@@ -970,14 +1039,14 @@ describe('ParseLiveQueryServer', function () {
9701039
parseLiveQueryServer._onAfterSave(message);
9711040

9721041
// Make sure we send create command to client
973-
setTimeout(function () {
974-
expect(client.pushCreate).toHaveBeenCalled();
975-
expect(client.pushEnter).not.toHaveBeenCalled();
976-
expect(client.pushUpdate).not.toHaveBeenCalled();
977-
expect(client.pushDelete).not.toHaveBeenCalled();
978-
expect(client.pushLeave).not.toHaveBeenCalled();
979-
done();
980-
}, jasmine.ASYNC_TEST_WAIT_TIME);
1042+
await timeout();
1043+
1044+
expect(client.pushCreate).toHaveBeenCalled();
1045+
expect(client.pushEnter).not.toHaveBeenCalled();
1046+
expect(client.pushUpdate).not.toHaveBeenCalled();
1047+
expect(client.pushDelete).not.toHaveBeenCalled();
1048+
expect(client.pushLeave).not.toHaveBeenCalled();
1049+
done();
9811050
});
9821051

9831052
it('can handle create command with fields', async done => {
@@ -1020,14 +1089,14 @@ describe('ParseLiveQueryServer', function () {
10201089
parseLiveQueryServer._onAfterSave(message);
10211090

10221091
// Make sure we send create command to client
1023-
setTimeout(function () {
1024-
expect(client.pushCreate).toHaveBeenCalled();
1025-
const args = parseWebSocket.send.calls.mostRecent().args;
1026-
const toSend = JSON.parse(args[0]);
1027-
expect(toSend.object).toBeDefined();
1028-
expect(toSend.original).toBeUndefined();
1029-
done();
1030-
}, jasmine.ASYNC_TEST_WAIT_TIME);
1092+
await timeout();
1093+
1094+
expect(client.pushCreate).toHaveBeenCalled();
1095+
const args = parseWebSocket.send.calls.mostRecent().args;
1096+
const toSend = JSON.parse(args[0]);
1097+
expect(toSend.object).toBeDefined();
1098+
expect(toSend.original).toBeUndefined();
1099+
done();
10311100
});
10321101

10331102
it('can match subscription for null or undefined parse object', function () {
@@ -1737,7 +1806,8 @@ describe('ParseLiveQueryServer', function () {
17371806
clientId,
17381807
requestId,
17391808
parseWebSocket,
1740-
query
1809+
query,
1810+
customQueryHashValue
17411811
) {
17421812
// If parseWebSocket is null, we use the default one
17431813
if (!parseWebSocket) {
@@ -1765,12 +1835,12 @@ describe('ParseLiveQueryServer', function () {
17651835
// Make mock subscription
17661836
const subscription = parseLiveQueryServer.subscriptions
17671837
.get(query.className)
1768-
.get(queryHashValue);
1838+
.get(customQueryHashValue || queryHashValue);
17691839
subscription.hasSubscribingClient = function () {
17701840
return false;
17711841
};
17721842
subscription.className = query.className;
1773-
subscription.hash = queryHashValue;
1843+
subscription.hash = customQueryHashValue || queryHashValue;
17741844
if (subscription.clientRequestIds && subscription.clientRequestIds.has(clientId)) {
17751845
subscription.clientRequestIds.get(clientId).push(requestId);
17761846
} else {

spec/helper.js

+2
Original file line numberDiff line numberDiff line change
@@ -515,3 +515,5 @@ jasmine.restoreLibrary = function (library, name) {
515515
}
516516
require(library)[name] = libraryCache[library][name];
517517
};
518+
519+
jasmine.timeout = t => new Promise(resolve => setTimeout(resolve, t));

src/LiveQuery/ParseLiveQueryServer.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,6 @@ class ParseLiveQueryServer {
298298
} else {
299299
return null;
300300
}
301-
message.event = type;
302301
res = {
303302
event: type,
304303
sessionToken: client.sessionToken,
@@ -334,8 +333,7 @@ class ParseLiveQueryServer {
334333
originalParseObject = res.original.toJSON();
335334
originalParseObject.className = res.original.className || className;
336335
}
337-
const functionName =
338-
'push' + message.event.charAt(0).toUpperCase() + message.event.slice(1);
336+
const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
339337
if (client[functionName]) {
340338
client[functionName](requestId, currentParseObject, originalParseObject);
341339
}

0 commit comments

Comments
 (0)