Bug 1074699 - Add createRoom and addCallback to LoopRooms API. r=dmose a=loop-only
authorPaul Kerr <paulrkerr@gmail.com>
Fri, 24 Oct 2014 11:28:36 +0100
changeset 233772 4f042a59a39ea6a34b8623381f31274f0a273f98
parent 233771 ba47f2f2dcf7c627d769c0aa94f3843f86892c63
child 233773 2f0874bd9c8d7ff85d45e89872922c42a8a01a4a
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdmose, loop-only
bugs1074699
milestone35.0a2
Bug 1074699 - Add createRoom and addCallback to LoopRooms API. r=dmose a=loop-only
browser/components/loop/LoopRooms.jsm
browser/components/loop/test/xpcshell/test_rooms_create.js
browser/components/loop/test/xpcshell/test_rooms_getdata.js
browser/components/loop/test/xpcshell/xpcshell.ini
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -11,30 +11,21 @@ Cu.import("resource://gre/modules/Servic
 
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService",
                                   "resource:///modules/loop/MozLoopService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LOOP_SESSION_TYPE",
                                   "resource:///modules/loop/MozLoopService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
                                   "resource:///modules/loop/MozLoopPushHandler.jsm");
 
-// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
-XPCOMUtils.defineLazyGetter(this, "log", () => {
-  let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
-  let consoleOptions = {
-    maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
-    prefix: "Loop",
-  };
-  return new ConsoleAPI(consoleOptions);
-});
-
 this.EXPORTED_SYMBOLS = ["LoopRooms", "roomsPushNotification"];
 
 let gRoomsListFetched = false;
 let gRooms = new Map();
+let gCallbacks = new Map();
 
   /**
    * Callback used to indicate changes to rooms data on the LoopServer.
    *
    * @param {Object} version Version number assigned to this change set.
    * @param {Object} channelID Notification channel identifier.
    *
    */
@@ -54,40 +45,42 @@ let LoopRoomsInternal = {
       // Fetch the rooms from the server.
       let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
                         LOOP_SESSION_TYPE.GUEST;
       let rooms = yield this.requestRoomList(sessionType);
       // Add each room to our in-memory Map using a locally unique
       // identifier.
       for (let room of rooms) {
         let id = MozLoopService.generateLocalID();
-        room.localRoomID = id;
+        room.localRoomId = id;
         // Next, request the detailed information for each room.
         // If the request fails the room data will not be added to the map.
         try {
           let details = yield this.requestRoomDetails(room.roomToken, sessionType);
           for (let attr in details) {
             room[attr] = details[attr]
           }
+          delete room.currSize; //This attribute will be eliminated in the next revision.
           gRooms.set(id, room);
         }
-        catch (error) {log.warn("failed GETing room details for roomToken = " + room.roomToken + ": ", error)}
+        catch (error) {MozLoopService.log.warn(
+          "failed GETing room details for roomToken = " + room.roomToken + ": ", error)}
       }
       callback(null, [...gRooms.values()]);
       return;
-      }.bind(this)).catch((error) => {log.error("getAll error:", error);
+      }.bind(this)).catch((error) => {MozLoopService.log.error("getAll error:", error);
                                       callback(error)});
     return;
   },
 
-  getRoomData: function(roomID, callback) {
-    if (gRooms.has(roomID)) {
-      callback(null, gRooms.get(roomID));
+  getRoomData: function(localRoomId, callback) {
+    if (gRooms.has(localRoomId)) {
+      callback(null, gRooms.get(localRoomId));
     } else {
-      callback(new Error("Room data not found or not fetched yet for room with ID " + roomID));
+      callback(new Error("Room data not found or not fetched yet for room with ID " + localRoomId));
     }
     return;
   },
 
   /**
    * Request list of all rooms associated with this account.
    *
    * @param {String} sessionType Indicates which hawkRequest endpoint to use.
@@ -125,16 +118,160 @@ let LoopRoomsInternal = {
    *
    * @param {Object} version Version number assigned to this change set.
    * @param {Object} channelID Notification channel identifier.
    *
    */
   onNotification: function(version, channelID) {
     return;
   },
+
+  createRoom: function(props, callback) {
+    // Always create a basic room record and launch the window, attaching
+    // the localRoomId. Later errors will be returned via the registered callback.
+    let localRoomId = MozLoopService.generateLocalID((id) => {gRooms.has(id)})
+    let room = {localRoomId : localRoomId};
+    for (let prop in props) {
+      room[prop] = props[prop]
+    }
+
+    gRooms.set(localRoomId, room);
+    this.addCallback(localRoomId, "RoomCreated", callback);
+    MozLoopService.openChatWindow(null, "", "about:loopconversation#room/" + localRoomId);
+
+    if (!"roomName" in props ||
+        !"expiresIn" in props ||
+        !"roomOwner" in props ||
+        !"maxSize" in props) {
+      this.postCallback(localRoomId, "RoomCreated",
+                        new Error("missing required room create property"));
+      return localRoomId;
+    }
+
+    let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
+                                                   LOOP_SESSION_TYPE.GUEST;
+
+    MozLoopService.hawkRequest(sessionType, "/rooms", "POST", props).then(
+      (response) => {
+        let data = JSON.parse(response.body);
+        for (let attr in data) {
+          room[attr] = data[attr]
+        }
+        delete room.expiresIn; //Do not keep this value - it is a request to the server
+        this.postCallback(localRoomId, "RoomCreated", null, room);
+      },
+      (error) => {
+        this.postCallback(localRoomId, "RoomCreated", error);
+      });
+
+    return localRoomId;
+  },
+
+  /**
+   * Send an update to the callbacks registered for a specific localRoomId
+   * for a callback type.
+   *
+   * The result set is always saved. Then each
+   * callback function that has been registered when this function is
+   * called will be called with the result set. Any new callback that
+   * is regsitered via addCallback will receive a copy of the last
+   * saved result set when registered. This allows the posting operation
+   * to complete before the callback is registered in an asynchronous
+   * operation.
+   *
+   * Callbacsk must be of the form:
+   *    function (error, success) {...}
+   *
+   * @param {String} localRoomId Local room identifier.
+   * @param {String} callbackName callback type
+   * @param {?Error} error result or null.
+   * @param {?Object} success result if error argument is null.
+   */
+  postCallback: function(localRoomId, callbackName, error, success) {
+    let roomCallbacks = gCallbacks.get(localRoomId);
+    if (!roomCallbacks) {
+      // No callbacks have been registered or results posted for this room.
+      // Initialize a record for this room and callbackName, saving the
+      // result set.
+      gCallbacks.set(localRoomId, new Map([[
+        callbackName,
+        { callbackList: [], result: { error: error, success: success } }]]));
+      return;
+    }
+
+    let namedCallback = roomCallbacks.get(callbackName);
+    // A callback of this name has not been registered.
+    if (!namedCallback) {
+      roomCallbacks.set(
+        callbackName,
+        {callbackList: [], result: {error: error, success: success}});
+      return;
+    }
+
+    // Record the latest result set.
+    namedCallback.result = {error: error, success: success};
+
+    // Call each registerd callback passing the new result posted.
+    namedCallback.callbackList.forEach((callback) => {
+      callback(error, success);
+    });
+  },
+
+  addCallback: function(localRoomId, callbackName, callback) {
+    let roomCallbacks = gCallbacks.get(localRoomId);
+    if (!roomCallbacks) {
+      // No callbacks have been registered or results posted for this room.
+      // Initialize a record for this room and callbackName.
+      gCallbacks.set(localRoomId, new Map([[
+        callbackName,
+        {callbackList: [callback]}]]));
+      return;
+    }
+
+    let namedCallback = roomCallbacks.get(callbackName);
+    // A callback of this name has not been registered.
+    if (!namedCallback) {
+      roomCallbacks.set(
+        callbackName,
+        {callbackList: [callback]});
+      return;
+    }
+
+    // Add this callback if not already in the array
+    if (namedCallback.callbackList.indexOf(callback) >= 0) {
+      return;
+    }
+    namedCallback.callbackList.push(callback);
+
+    // If a result has been posted for this callback
+    // send it using this new callback function.
+    let result = namedCallback.result;
+    if (result) {
+      callback(result.error, result.success);
+    }
+  },
+
+  deleteCallback: function(localRoomId, callbackName, callback) {
+    let roomCallbacks = gCallbacks.get(localRoomId);
+    if (!roomCallbacks) {
+      return;
+    }
+
+    let namedCallback = roomCallbacks.get(callbackName);
+    if (!namedCallback) {
+      return;
+    }
+
+    let i = namedCallback.callbackList.indexOf(callback);
+    if (i >= 0) {
+      namedCallback.callbackList.splice(i, 1);
+    }
+
+    return;
+  },
 };
 Object.freeze(LoopRoomsInternal);
 
 /**
  * The LoopRooms class.
  *
  * Each method that is a member of this class requires the last argument to be a
  * callback Function. MozLoopAPI will cause things to break if this invariant is
@@ -151,20 +288,56 @@ this.LoopRooms = {
    */
   getAll: function(callback) {
     return LoopRoomsInternal.getAll(callback);
   },
 
   /**
    * Return the current stored version of the data for the indicated room.
    *
-   * @param {String} roomID Local room identifier
+   * @param {String} localRoomId Local room identifier
    * @param {Function} callback Function that will be invoked once the operation
    *                            finished. The first argument passed will be an
    *                            `Error` object or `null`. The second argument will
    *                            be the list of rooms, if it was fetched successfully.
    */
-  getRoomData: function(roomID, callback) {
-    return LoopRoomsInternal.getRoomData(roomID, callback);
+  getRoomData: function(localRoomId, callback) {
+    return LoopRoomsInternal.getRoomData(localRoomId, callback);
+  },
+
+  /**
+   * Create a room. Will both open a chat window for the new room
+   * and perform an exchange with the LoopServer to create the room.
+   * for a callback type. Callback must be of the form:
+   *    function (error, success) {...}
+   *
+   * @param {Object} room properties to be sent to the LoopServer
+   * @param {Function} callback Must be of the form: function (error, success) {...}
+   *
+   * @returns {String} localRoomId assigned to this new room.
+   */
+  createRoom: function(roomProps, callback) {
+    return LoopRoomsInternal.createRoom(roomProps, callback);
+  },
+
+  /**
+   * Register a callback of a specified type with a localRoomId.
+   *
+   * @param {String} localRoomId Local room identifier.
+   * @param {String} callbackName callback type
+   * @param {Function} callback Must be of the form: function (error, success) {...}
+   */
+  addCallback: function(localRoomId, callbackName, callback) {
+    return LoopRoomsInternal.addCallback(localRoomId, callbackName, callback);
+  },
+
+  /**
+   * Un-register and delete a callback of a specified type for a localRoomId.
+   *
+   * @param {String} localRoomId Local room identifier.
+   * @param {String} callbackName callback type
+   * @param {Function} callback Previously passed to addCallback().
+   */
+  deleteCallback: function(localRoomId, callbackName, callback) {
+    return LoopRoomsInternal.deleteCallback(localRoomId, callbackName, callback);
   },
 };
 Object.freeze(LoopRooms);
-
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/test_rooms_create.js
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Cu.import("resource://services-common/utils.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Chat",
+                                  "resource:///modules/Chat.jsm");
+let hasTheseProps = function(a, b) {
+  for (let prop in a) {
+    if (a[prop] != b[prop]) {
+      return false;
+    }
+  }
+  return true;
+}
+
+let openChatOrig = Chat.open;
+
+add_test(function test_openRoomsWindow() {
+  let roomProps = {roomName: "UX Discussion",
+                   expiresIn: 5,
+                   roomOwner: "Alexis",
+                   maxSize: 2}
+
+  let roomData = {roomToken: "_nxD4V4FflQ",
+                  roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
+                  expiresAt: 1405534180}
+
+  loopServer.registerPathHandler("/rooms", (request, response) => {
+    if (!request.bodyInputStream) {
+      do_throw("empty request body");
+    }
+    let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+    let data = JSON.parse(body);
+    do_check_true(hasTheseProps(roomProps, data));
+
+    response.setStatusLine(null, 200, "OK");
+    response.write(JSON.stringify(roomData));
+    response.processAsync();
+    response.finish();
+  });
+
+  MozLoopService.register(mockPushHandler).then(() => {
+    let opened = false;
+    let created = false;
+    let urlPieces = [];
+
+    Chat.open = function(contentWindow, origin, title, url) {
+      urlPieces = url.split('/');
+      do_check_eq(urlPieces[0], "about:loopconversation#room");
+      opened = true;
+    };
+
+    let returnedID = LoopRooms.createRoom(roomProps, (error, data) => {
+      do_check_false(error);
+      do_check_true(data);
+      do_check_true(hasTheseProps(roomData, data));
+      do_check_eq(data.localRoomId, urlPieces[1]);
+      created = true;
+    });
+
+    waitForCondition(function() created && opened).then(() => {
+      do_check_true(opened, "should open a chat window");
+      do_check_eq(returnedID, urlPieces[1]);
+
+      // Verify that a delayed callback, when attached,
+      // received the same data.
+      LoopRooms.addCallback(
+        urlPieces[1], "RoomCreated",
+        (error, data) => {
+          do_check_false(error);
+          do_check_true(data);
+          do_check_true(hasTheseProps(roomData, data));
+          do_check_eq(data.localRoomId, urlPieces[1]);
+        });
+
+      run_next_test();
+    }, () => {
+      do_throw("should have opened a chat window");
+    });
+
+  });
+});
+
+function run_test()
+{
+  setupFakeLoopServer();
+  mockPushHandler.registrationPushURL = kEndPointUrl;
+
+  loopServer.registerPathHandler("/registration", (request, response) => {
+    response.setStatusLine(null, 200, "OK");
+    response.processAsync();
+    response.finish();
+  });
+
+  do_register_cleanup(function() {
+    // Revert original Chat.open implementation
+    Chat.open = openChatOrig;
+  });
+
+  run_next_test();
+}
--- a/browser/components/loop/test/xpcshell/test_rooms_getdata.js
+++ b/browser/components/loop/test/xpcshell/test_rooms_getdata.js
@@ -89,24 +89,26 @@ add_test(function test_getAllRooms() {
       do_check_false(error);
       do_check_true(rooms);
       do_check_eq(rooms.length, 3);
       do_check_eq(rooms[0].roomName, "First Room Name");
       do_check_eq(rooms[1].roomName, "Second Room Name");
       do_check_eq(rooms[2].roomName, "Third Room Name");
 
       let room = rooms[0];
-      do_check_true(room.localRoomID);
+      do_check_true(room.localRoomId);
+      do_check_false(room.currSize);
+      delete roomList[0].currSize;
       do_check_true(hasTheseProps(roomList[0], room));
       delete roomDetail.roomName;
       delete room.participants;
       delete roomDetail.participants;
       do_check_true(hasTheseProps(roomDetail, room));
 
-      LoopRooms.getRoomData(room.localRoomID, (error, roomData) => {
+      LoopRooms.getRoomData(room.localRoomId, (error, roomData) => {
         do_check_false(error);
         do_check_true(hasTheseProps(room, roomData));
 
         run_next_test();
       });
     });
   });
 });
--- a/browser/components/loop/test/xpcshell/xpcshell.ini
+++ b/browser/components/loop/test/xpcshell/xpcshell.ini
@@ -17,8 +17,9 @@ skip-if = toolkit == 'gonk'
 [test_loopservice_registration.js]
 [test_loopservice_restart.js]
 [test_loopservice_token_invalid.js]
 [test_loopservice_token_save.js]
 [test_loopservice_token_send.js]
 [test_loopservice_token_validation.js]
 [test_loopservice_busy.js]
 [test_rooms_getdata.js]
+[test_rooms_create.js]