mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-10 11:55:49 +00:00
Bug 1074666: add support for room updates, fix event dispatching and support room participant processing. r=Standard8
This commit is contained in:
parent
421d3a675c
commit
bffc95cab8
@ -41,6 +41,61 @@ const extend = function(target, source) {
|
||||
return target;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether a participant is already part of a room.
|
||||
*
|
||||
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#User_Identification_in_a_Room
|
||||
*
|
||||
* @param {Object} room A room object that contains a list of current participants
|
||||
* @param {Object} participant Participant to check if it's already there
|
||||
* @returns {Boolean} TRUE when the participant is already a member of the room,
|
||||
* FALSE when it's not.
|
||||
*/
|
||||
const containsParticipant = function(room, participant) {
|
||||
for (let user of room.participants) {
|
||||
if (user.roomConnectionId == participant.roomConnectionId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares the list of participants of the room currently in the cache and an
|
||||
* updated version of that room. When a new participant is found, the 'joined'
|
||||
* event is emitted. When a participant is not found in the update, it emits a
|
||||
* 'left' event.
|
||||
*
|
||||
* @param {Object} room A room object to compare the participants list
|
||||
* against
|
||||
* @param {Object} updatedRoom A room object that contains the most up-to-date
|
||||
* list of participants
|
||||
*/
|
||||
const checkForParticipantsUpdate = function(room, updatedRoom) {
|
||||
// Partially fetched rooms don't contain the participants list yet. Skip the
|
||||
// check for now.
|
||||
if (!("participants" in room)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let participant;
|
||||
// Check for participants that joined.
|
||||
for (participant of updatedRoom.participants) {
|
||||
if (!containsParticipant(room, participant)) {
|
||||
eventEmitter.emit("joined", room.roomToken, participant);
|
||||
eventEmitter.emit("joined:" + room.roomToken, participant);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for participants that left.
|
||||
for (participant of room.participants) {
|
||||
if (!containsParticipant(updatedRoom, participant)) {
|
||||
eventEmitter.emit("left", room.roomToken, participant);
|
||||
eventEmitter.emit("left:" + room.roomToken, participant);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The Rooms class.
|
||||
*
|
||||
@ -85,11 +140,23 @@ let LoopRoomsInternal = {
|
||||
throw new Error("Missing array of rooms in response.");
|
||||
}
|
||||
|
||||
// Next, request the detailed information for each room. If the request
|
||||
// fails the room data will not be added to the map.
|
||||
for (let room of roomsList) {
|
||||
// See if we already have this room in our cache.
|
||||
let orig = this.rooms.get(room.roomToken);
|
||||
if (orig) {
|
||||
checkForParticipantsUpdate(orig, room);
|
||||
}
|
||||
this.rooms.set(room.roomToken, room);
|
||||
yield LoopRooms.promise("get", room.roomToken);
|
||||
// When a version is specified, all the data is already provided by this
|
||||
// request.
|
||||
if (version) {
|
||||
eventEmitter.emit("update", room);
|
||||
eventEmitter.emit("update" + ":" + room.roomToken, room);
|
||||
} else {
|
||||
// Next, request the detailed information for each room. If the request
|
||||
// fails the room data will not be added to the map.
|
||||
yield LoopRooms.promise("get", room.roomToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the 'dirty' flag back to FALSE, since the list is as fresh as can be now.
|
||||
@ -113,25 +180,35 @@ let LoopRoomsInternal = {
|
||||
get: function(roomToken, callback) {
|
||||
let room = this.rooms.has(roomToken) ? this.rooms.get(roomToken) : {};
|
||||
// Check if we need to make a request to the server to collect more room data.
|
||||
if (!room || gDirty || !("participants" in room)) {
|
||||
let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
|
||||
LOOP_SESSION_TYPE.GUEST;
|
||||
MozLoopService.hawkRequest(sessionType, "/rooms/" + encodeURIComponent(roomToken), "GET")
|
||||
.then(response => {
|
||||
let eventName = ("roomToken" in room) ? "add" : "update";
|
||||
extend(room, JSON.parse(response.body));
|
||||
// Remove the `currSize` for posterity.
|
||||
if ("currSize" in room) {
|
||||
delete room.currSize;
|
||||
}
|
||||
this.rooms.set(roomToken, room);
|
||||
|
||||
eventEmitter.emit(eventName, room);
|
||||
callback(null, room);
|
||||
}, err => callback(err)).catch(err => callback(err));
|
||||
} else {
|
||||
let needsUpdate = !("participants" in room);
|
||||
if (!gDirty && !needsUpdate) {
|
||||
// Dirty flag is not set AND the necessary data is available, so we can
|
||||
// simply return the room.
|
||||
callback(null, room);
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
|
||||
LOOP_SESSION_TYPE.GUEST;
|
||||
MozLoopService.hawkRequest(sessionType, "/rooms/" + encodeURIComponent(roomToken), "GET")
|
||||
.then(response => {
|
||||
let data = JSON.parse(response.body);
|
||||
|
||||
room.roomToken = roomToken;
|
||||
checkForParticipantsUpdate(room, data);
|
||||
extend(room, data);
|
||||
|
||||
// Remove the `currSize` for posterity.
|
||||
if ("currSize" in room) {
|
||||
delete room.currSize;
|
||||
}
|
||||
this.rooms.set(roomToken, room);
|
||||
|
||||
let eventName = !needsUpdate ? "update" : "add";
|
||||
eventEmitter.emit(eventName, room);
|
||||
eventEmitter.emit(eventName + ":" + roomToken, room);
|
||||
callback(null, room);
|
||||
}, err => callback(err)).catch(err => callback(err));
|
||||
},
|
||||
|
||||
/**
|
||||
@ -194,10 +271,13 @@ Object.freeze(LoopRoomsInternal);
|
||||
* LoopRooms implements the EventEmitter interface by exposing three methods -
|
||||
* `on`, `once` and `off` - to subscribe to events.
|
||||
* At this point the following events may be subscribed to:
|
||||
* - 'add': A new room object was successfully added to the data store.
|
||||
* - 'remove': A room was successfully removed from the data store.
|
||||
* - 'update': A room object was successfully updated with changed
|
||||
* properties in the data store.
|
||||
* - 'add[:{room-id}]': A new room object was successfully added to the data
|
||||
* store.
|
||||
* - 'remove[:{room-id}]': A room was successfully removed from the data store.
|
||||
* - 'update[:{room-id}]': A room object was successfully updated with changed
|
||||
* properties in the data store.
|
||||
* - 'joined[:{room-id}]': A participant joined a room.
|
||||
* - 'left[:{room-id}]': A participant left a room.
|
||||
*
|
||||
* See the internal code for the API documentation.
|
||||
*/
|
||||
|
@ -54,6 +54,38 @@ let roomDetail = {
|
||||
}]
|
||||
};
|
||||
|
||||
const kRoomUpdates = {
|
||||
"1": {
|
||||
participants: []
|
||||
},
|
||||
"2": {
|
||||
participants: [{
|
||||
displayName: "Alexis",
|
||||
account: "alexis@example.com",
|
||||
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
|
||||
}]
|
||||
},
|
||||
"3": {
|
||||
participants: [{
|
||||
displayName: "Adam",
|
||||
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
|
||||
}]
|
||||
},
|
||||
"4": {
|
||||
participants: [{
|
||||
displayName: "Adam",
|
||||
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
|
||||
}, {
|
||||
displayName: "Alexis",
|
||||
account: "alexis@example.com",
|
||||
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
|
||||
}, {
|
||||
displayName: "Ruharb",
|
||||
roomConnectionId: "5de6281c-6568-455f-af08-c0b0a973100e"
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const kCreateRoomProps = {
|
||||
roomName: "UX Discussion",
|
||||
expiresIn: 5,
|
||||
@ -67,6 +99,76 @@ const kCreateRoomData = {
|
||||
expiresAt: 1405534180
|
||||
};
|
||||
|
||||
const normalizeRoom = function(room) {
|
||||
delete room.currSize;
|
||||
if (!("participants" in room)) {
|
||||
let name = room.roomName;
|
||||
for (let key of Object.getOwnPropertyNames(roomDetail)) {
|
||||
room[key] = roomDetail[key];
|
||||
}
|
||||
room.roomName = name;
|
||||
}
|
||||
return room;
|
||||
};
|
||||
|
||||
const compareRooms = function(room1, room2) {
|
||||
Assert.deepEqual(normalizeRoom(room1), normalizeRoom(room2));
|
||||
};
|
||||
|
||||
// LoopRooms emits various events. Test if they work as expected here.
|
||||
let gExpectedAdds = [];
|
||||
let gExpectedUpdates = [];
|
||||
let gExpectedJoins = {};
|
||||
let gExpectedLeaves = {};
|
||||
|
||||
const onRoomAdded = function(e, room) {
|
||||
let expectedIds = gExpectedAdds.map(room => room.roomToken);
|
||||
let idx = expectedIds.indexOf(room.roomToken);
|
||||
Assert.ok(idx > -1, "Added room should be expected");
|
||||
let expected = gExpectedAdds[idx];
|
||||
compareRooms(room, expected);
|
||||
gExpectedAdds.splice(idx, 1);
|
||||
};
|
||||
|
||||
const onRoomUpdated = function(e, room) {
|
||||
let idx = gExpectedUpdates.indexOf(room.roomToken);
|
||||
Assert.ok(idx > -1, "Updated room should be expected");
|
||||
gExpectedUpdates.splice(idx, 1);
|
||||
};
|
||||
|
||||
const onRoomJoined = function(e, roomToken, participant) {
|
||||
let participants = gExpectedJoins[roomToken];
|
||||
Assert.ok(participants, "Participant should be expected to join");
|
||||
let idx = participants.indexOf(participant.roomConnectionId);
|
||||
Assert.ok(idx > -1, "Participant should be expected to join");
|
||||
participants.splice(idx, 1);
|
||||
if (!participants.length) {
|
||||
delete gExpectedJoins[roomToken];
|
||||
}
|
||||
};
|
||||
|
||||
const onRoomLeft = function(e, roomToken, participant) {
|
||||
let participants = gExpectedLeaves[roomToken];
|
||||
Assert.ok(participants, "Participant should be expected to leave");
|
||||
let idx = participants.indexOf(participant.roomConnectionId);
|
||||
Assert.ok(idx > -1, "Participant should be expected to leave");
|
||||
participants.splice(idx, 1);
|
||||
if (!participants.length) {
|
||||
delete gExpectedLeaves[roomToken];
|
||||
}
|
||||
};
|
||||
|
||||
const parseQueryString = function(qs) {
|
||||
let map = {};
|
||||
let parts = qs.split("=");
|
||||
for (let i = 0, l = parts.length; i < l; ++i) {
|
||||
if (i % 2 === 1) {
|
||||
map[parts[i - 1]] = parts[i];
|
||||
}
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
add_task(function* setup_server() {
|
||||
loopServer.registerPathHandler("/registration", (req, res) => {
|
||||
res.setStatusLine(null, 200, "OK");
|
||||
@ -85,7 +187,14 @@ add_task(function* setup_server() {
|
||||
|
||||
res.write(JSON.stringify(kCreateRoomData));
|
||||
} else {
|
||||
res.write(JSON.stringify([...kRooms.values()]));
|
||||
if (req.queryString) {
|
||||
let qs = parseQueryString(req.queryString);
|
||||
let room = kRooms.get("_nxD4V4FflQ");
|
||||
room.participants = kRoomUpdates[qs.version].participants;
|
||||
res.write(JSON.stringify([room]));
|
||||
} else {
|
||||
res.write(JSON.stringify([...kRooms.values()]));
|
||||
}
|
||||
}
|
||||
|
||||
res.processAsync();
|
||||
@ -119,27 +228,13 @@ add_task(function* setup_server() {
|
||||
res.processAsync();
|
||||
res.finish();
|
||||
});
|
||||
|
||||
yield MozLoopService.promiseRegisteredWithServers();
|
||||
});
|
||||
|
||||
const normalizeRoom = function(room) {
|
||||
delete room.currSize;
|
||||
if (!("participants" in room)) {
|
||||
let name = room.roomName;
|
||||
for (let key of Object.getOwnPropertyNames(roomDetail)) {
|
||||
room[key] = roomDetail[key];
|
||||
}
|
||||
room.roomName = name;
|
||||
}
|
||||
return room;
|
||||
};
|
||||
|
||||
const compareRooms = function(room1, room2) {
|
||||
Assert.deepEqual(normalizeRoom(room1), normalizeRoom(room2));
|
||||
};
|
||||
|
||||
// Test if fetching a list of all available rooms works correctly.
|
||||
add_task(function* test_getAllRooms() {
|
||||
yield MozLoopService.promiseRegisteredWithServers();
|
||||
|
||||
gExpectedAdds.push(...kRooms.values());
|
||||
let rooms = yield LoopRooms.promise("getAll");
|
||||
Assert.equal(rooms.length, 3);
|
||||
for (let room of rooms) {
|
||||
@ -147,30 +242,27 @@ add_task(function* test_getAllRooms() {
|
||||
}
|
||||
});
|
||||
|
||||
// Test if fetching a room works correctly.
|
||||
add_task(function* test_getRoom() {
|
||||
yield MozLoopService.promiseRegisteredWithServers();
|
||||
|
||||
let roomToken = "_nxD4V4FflQ";
|
||||
let room = yield LoopRooms.promise("get", roomToken);
|
||||
Assert.deepEqual(room, kRooms.get(roomToken));
|
||||
});
|
||||
|
||||
// Test if fetching a room with incorrect token or return values yields an error.
|
||||
add_task(function* test_errorStates() {
|
||||
yield Assert.rejects(LoopRooms.promise("get", "error401"), /Not Found/, "Fetching a non-existent room should fail");
|
||||
yield Assert.rejects(LoopRooms.promise("get", "errorMalformed"), /SyntaxError/, "Wrong message format should reject");
|
||||
});
|
||||
|
||||
// Test if creating a new room works as expected.
|
||||
add_task(function* test_createRoom() {
|
||||
let eventCalled = false;
|
||||
LoopRooms.once("add", (e, room) => {
|
||||
compareRooms(room, kCreateRoomProps);
|
||||
eventCalled = true;
|
||||
});
|
||||
gExpectedAdds.push(kCreateRoomProps);
|
||||
let room = yield LoopRooms.promise("create", kCreateRoomProps);
|
||||
compareRooms(room, kCreateRoomProps);
|
||||
Assert.ok(eventCalled, "Event should have fired");
|
||||
});
|
||||
|
||||
// Test if opening a new room window works correctly.
|
||||
add_task(function* test_openRoom() {
|
||||
let openedUrl;
|
||||
Chat.open = function(contentWindow, origin, title, url) {
|
||||
@ -189,13 +281,58 @@ add_task(function* test_openRoom() {
|
||||
Assert.equal(windowData.roomToken, "fakeToken", "window data should have the roomToken");
|
||||
});
|
||||
|
||||
// Test if push updates function as expected.
|
||||
add_task(function* test_roomUpdates() {
|
||||
gExpectedUpdates.push("_nxD4V4FflQ");
|
||||
gExpectedLeaves["_nxD4V4FflQ"] = [
|
||||
"2a1787a6-4a73-43b5-ae3e-906ec1e763cb",
|
||||
"781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
|
||||
];
|
||||
roomsPushNotification("1");
|
||||
yield waitForCondition(() => Object.getOwnPropertyNames(gExpectedLeaves).length === 0);
|
||||
|
||||
gExpectedUpdates.push("_nxD4V4FflQ");
|
||||
gExpectedJoins["_nxD4V4FflQ"] = ["2a1787a6-4a73-43b5-ae3e-906ec1e763cb"];
|
||||
roomsPushNotification("2");
|
||||
yield waitForCondition(() => Object.getOwnPropertyNames(gExpectedJoins).length === 0);
|
||||
|
||||
gExpectedUpdates.push("_nxD4V4FflQ");
|
||||
gExpectedJoins["_nxD4V4FflQ"] = ["781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"];
|
||||
gExpectedLeaves["_nxD4V4FflQ"] = ["2a1787a6-4a73-43b5-ae3e-906ec1e763cb"];
|
||||
roomsPushNotification("3");
|
||||
yield waitForCondition(() => Object.getOwnPropertyNames(gExpectedLeaves).length === 0);
|
||||
|
||||
gExpectedUpdates.push("_nxD4V4FflQ");
|
||||
gExpectedJoins["_nxD4V4FflQ"] = [
|
||||
"2a1787a6-4a73-43b5-ae3e-906ec1e763cb",
|
||||
"5de6281c-6568-455f-af08-c0b0a973100e"];
|
||||
roomsPushNotification("4");
|
||||
yield waitForCondition(() => Object.getOwnPropertyNames(gExpectedJoins).length === 0);
|
||||
});
|
||||
|
||||
// Test if the event emitter implementation doesn't leak and is working as expected.
|
||||
add_task(function* () {
|
||||
Assert.strictEqual(gExpectedAdds.length, 0, "No room additions should be expected anymore");
|
||||
Assert.strictEqual(gExpectedUpdates.length, 0, "No room updates should be expected anymore");
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
do_register_cleanup(function() {
|
||||
setupFakeLoopServer();
|
||||
|
||||
LoopRooms.on("add", onRoomAdded);
|
||||
LoopRooms.on("update", onRoomUpdated);
|
||||
LoopRooms.on("joined", onRoomJoined);
|
||||
LoopRooms.on("left", onRoomLeft);
|
||||
|
||||
do_register_cleanup(function () {
|
||||
// Revert original Chat.open implementation
|
||||
Chat.open = openChatOrig;
|
||||
});
|
||||
|
||||
setupFakeLoopServer();
|
||||
LoopRooms.off("add", onRoomAdded);
|
||||
LoopRooms.off("update", onRoomUpdated);
|
||||
LoopRooms.off("joined", onRoomJoined);
|
||||
LoopRooms.off("left", onRoomLeft);
|
||||
});
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user