Bug 1497457 - Allow to remove one time listeners on event-source;r=ochameau
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 27 Nov 2018 10:13:47 +0000
changeset 504657 b078b2df55db814d01c7402587a3dd5c0b6dff36
parent 504656 2859c48a1a810f72032d9d592c63900390e689a0
child 504658 fe4c1c6d7ebf20056824d489e085856b507f4bdf
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1497457
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1497457 - Allow to remove one time listeners on event-source;r=ochameau Differential Revision: https://phabricator.services.mozilla.com/D11992
devtools/shared/client/event-source.js
devtools/shared/tests/unit/test_eventsource.js
devtools/shared/tests/unit/xpcshell.ini
--- a/devtools/shared/client/event-source.js
+++ b/devtools/shared/client/event-source.js
@@ -22,50 +22,54 @@ function eventSource(proto) {
    * Add a listener to the event source for a given event.
    *
    * @param name string
    *        The event to listen for.
    * @param listener function
    *        Called when the event is fired. If the same listener
    *        is added more than once, it will be called once per
    *        addListener call.
+   * @param key function (optional)
+   *        Key to use for removeListener, defaults to the listener. Used by helper method
+   *        addOneTimeListener, which creates a custom listener. Use the original listener
+   *        as key to allow to remove oneTimeListeners.
    */
-  proto.addListener = function(name, listener) {
+  proto.addListener = function(name, listener, key = listener) {
     if (typeof listener != "function") {
       throw TypeError("Listeners must be functions.");
     }
 
     if (!this._listeners) {
       this._listeners = {};
     }
 
-    this._getListeners(name).push(listener);
+    this._getListeners(name).push({ key, callback: listener });
   };
 
   /**
    * Add a listener to the event source for a given event. The
    * listener will be removed after it is called for the first time.
    *
    * @param name string
    *        The event to listen for.
    * @param listener function
    *        Called when the event is fired.
    * @returns Promise
    *          Resolved with an array of the arguments of the event.
    */
   proto.addOneTimeListener = function(name, listener) {
     return new Promise(resolve => {
-      const l = (eventName, ...rest) => {
-        this.removeListener(name, l);
+      const oneTimeListener = (eventName, ...rest) => {
+        this.removeListener(name, listener);
         if (listener) {
           listener(eventName, ...rest);
         }
         resolve(rest[0]);
       };
-      this.addListener(name, l);
+      this.addListener(name, oneTimeListener, listener);
     });
   };
 
   /**
    * Remove a listener from the event source previously added with
    * addListener().
    *
    * @param name string
@@ -78,17 +82,17 @@ function eventSource(proto) {
     if (!this._listeners || (listener && !this._listeners[name])) {
       return;
     }
 
     if (!listener) {
       this._listeners[name] = [];
     } else {
       this._listeners[name] =
-        this._listeners[name].filter(l => l != listener);
+        this._listeners[name].filter(l => l.key != listener);
     }
   };
 
   /**
    * Returns the listeners for the specified event name. If none are defined it
    * initializes an empty list and returns that.
    *
    * @param name string
@@ -116,17 +120,17 @@ function eventSource(proto) {
       return;
     }
 
     const name = arguments[0];
     const listeners = this._getListeners(name).slice(0);
 
     for (const listener of listeners) {
       try {
-        listener.apply(null, arguments);
+        listener.callback.apply(null, arguments);
       } catch (e) {
         // Prevent a bad listener from interfering with the others.
         DevToolsUtils.reportException("notify event '" + name + "'", e);
       }
     }
   };
 }
 
new file mode 100644
--- /dev/null
+++ b/devtools/shared/tests/unit/test_eventsource.js
@@ -0,0 +1,67 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const eventSource = require("devtools/shared/client/event-source");
+
+// Test basic event-source APIs:
+// - addListener
+// - removeListener
+// - addOneTimeListener
+
+add_task(function() {
+  // Create a basic test object that can emit events using event-source.js
+  class TestClass {}
+  eventSource(TestClass.prototype);
+  const testObject = new TestClass();
+
+  testBasicAddRemoveListener(testObject);
+
+  info("Check that one time listeners are only triggered once");
+  testOneTimeListener(testObject);
+
+  info("Check that one time listeners can be removed");
+  testRemoveOneTimeListener(testObject);
+});
+
+function testBasicAddRemoveListener(testObject) {
+  let eventsReceived = 0;
+  const onTestEvent = () => eventsReceived++;
+
+  testObject.addListener("event-testBasicAddRemoveListener", onTestEvent);
+  testObject.emit("event-testBasicAddRemoveListener");
+  ok(eventsReceived === 1, "Event listener was triggered");
+
+  testObject.emit("event-testBasicAddRemoveListener");
+  ok(eventsReceived === 2, "Event listener was triggered again");
+
+  testObject.removeListener("event-testBasicAddRemoveListener", onTestEvent);
+  testObject.emit("event-testBasicAddRemoveListener");
+  ok(eventsReceived === 2, "Event listener was not triggered anymore");
+}
+
+function testOneTimeListener(testObject) {
+  let eventsReceived = 0;
+  const onTestEvent = () => eventsReceived++;
+
+  testObject.addOneTimeListener("event-testOneTimeListener", onTestEvent);
+  testObject.emit("event-testOneTimeListener");
+  ok(eventsReceived === 1, "Event listener was triggered");
+
+  testObject.emit("event-testOneTimeListener");
+  ok(eventsReceived === 1, "Event listener was not triggered again");
+
+  testObject.removeListener("event-testOneTimeListener", onTestEvent);
+}
+
+function testRemoveOneTimeListener(testObject) {
+  let eventsReceived = 0;
+  const onTestEvent = () => eventsReceived++;
+
+  testObject.addOneTimeListener("event-testRemoveOneTimeListener", onTestEvent);
+  testObject.removeListener("event-testRemoveOneTimeListener", onTestEvent);
+  testObject.emit("event-testRemoveOneTimeListener");
+  ok(eventsReceived === 0, "Event listener was already removed");
+}
--- a/devtools/shared/tests/unit/xpcshell.ini
+++ b/devtools/shared/tests/unit/xpcshell.ini
@@ -11,16 +11,17 @@ support-files =
 [test_css-properties-db.js]
 # This test only enforces that the CSS database is up to date with nightly. The DB is
 # only used when inspecting a target that doesn't support the getCSSDatabase actor.
 # CSS properties are behind compile-time flags, and there is no automatic rebuild
 # process for uplifts, so this test breaks on uplift.
 run-if = nightly_build
 [test_eventemitter_basic.js]
 [test_eventemitter_static.js]
+[test_eventsource.js]
 [test_fetch-bom.js]
 [test_fetch-chrome.js]
 [test_fetch-file.js]
 [test_fetch-http.js]
 [test_fetch-resource.js]
 [test_flatten.js]
 [test_indentation.js]
 [test_independent_loaders.js]