event-emitter.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. (function (factory) {
  6. // This file can be loaded in several different ways. It can be
  7. // require()d, either from the main thread or from a worker thread;
  8. // or it can be imported via Cu.import. These different forms
  9. // explain some of the hairiness of this code.
  10. //
  11. // It's important for the devtools-as-html project that a require()
  12. // on the main thread not use any chrome privileged APIs. Instead,
  13. // the body of the main function can only require() (not Cu.import)
  14. // modules that are available in the devtools content mode. This,
  15. // plus the lack of |console| in workers, results in some gyrations
  16. // in the definition of |console|.
  17. if (this.module && module.id.indexOf("event-emitter") >= 0) {
  18. let console;
  19. if (isWorker) {
  20. console = {
  21. error: () => {}
  22. };
  23. } else {
  24. console = this.console;
  25. }
  26. // require
  27. factory.call(this, require, exports, module, console);
  28. } else {
  29. // Cu.import. This snippet implements a sort of miniature loader,
  30. // which is responsible for appropriately translating require()
  31. // requests from the client function. This code can use
  32. // Cu.import, because it is never run in the devtools-in-content
  33. // mode.
  34. this.isWorker = false;
  35. const Cu = Components.utils;
  36. let console = Cu.import("resource://gre/modules/Console.jsm", {}).console;
  37. // Bug 1259045: This module is loaded early in firefox startup as a JSM,
  38. // but it doesn't depends on any real module. We can save a few cycles
  39. // and bytes by not loading Loader.jsm.
  40. let require = function (module) {
  41. switch (module) {
  42. case "devtools/shared/defer":
  43. return Cu.import("resource://gre/modules/Promise.jsm", {}).Promise.defer;
  44. case "Services":
  45. return Cu.import("resource://gre/modules/Services.jsm", {}).Services;
  46. case "devtools/shared/platform/stack": {
  47. let obj = {};
  48. Cu.import("resource://devtools/shared/platform/chrome/stack.js", obj);
  49. return obj;
  50. }
  51. }
  52. return null;
  53. };
  54. factory.call(this, require, this, { exports: this }, console);
  55. this.EXPORTED_SYMBOLS = ["EventEmitter"];
  56. }
  57. }).call(this, function (require, exports, module, console) {
  58. // ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
  59. // After this point the code may not use Cu.import, and should only
  60. // require() modules that are "clean-for-content".
  61. let EventEmitter = this.EventEmitter = function () {};
  62. module.exports = EventEmitter;
  63. // See comment in JSM module boilerplate when adding a new dependency.
  64. const Services = require("Services");
  65. const defer = require("devtools/shared/defer");
  66. const { describeNthCaller } = require("devtools/shared/platform/stack");
  67. let loggingEnabled = true;
  68. if (!isWorker) {
  69. loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
  70. Services.prefs.addObserver("devtools.dump.emit", {
  71. observe: () => {
  72. loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
  73. }
  74. }, false);
  75. }
  76. /**
  77. * Decorate an object with event emitter functionality.
  78. *
  79. * @param Object objectToDecorate
  80. * Bind all public methods of EventEmitter to
  81. * the objectToDecorate object.
  82. */
  83. EventEmitter.decorate = function (objectToDecorate) {
  84. let emitter = new EventEmitter();
  85. objectToDecorate.on = emitter.on.bind(emitter);
  86. objectToDecorate.off = emitter.off.bind(emitter);
  87. objectToDecorate.once = emitter.once.bind(emitter);
  88. objectToDecorate.emit = emitter.emit.bind(emitter);
  89. };
  90. EventEmitter.prototype = {
  91. /**
  92. * Connect a listener.
  93. *
  94. * @param string event
  95. * The event name to which we're connecting.
  96. * @param function listener
  97. * Called when the event is fired.
  98. */
  99. on(event, listener) {
  100. if (!this._eventEmitterListeners) {
  101. this._eventEmitterListeners = new Map();
  102. }
  103. if (!this._eventEmitterListeners.has(event)) {
  104. this._eventEmitterListeners.set(event, []);
  105. }
  106. this._eventEmitterListeners.get(event).push(listener);
  107. },
  108. /**
  109. * Listen for the next time an event is fired.
  110. *
  111. * @param string event
  112. * The event name to which we're connecting.
  113. * @param function listener
  114. * (Optional) Called when the event is fired. Will be called at most
  115. * one time.
  116. * @return promise
  117. * A promise which is resolved when the event next happens. The
  118. * resolution value of the promise is the first event argument. If
  119. * you need access to second or subsequent event arguments (it's rare
  120. * that this is needed) then use listener
  121. */
  122. once(event, listener) {
  123. let deferred = defer();
  124. let handler = (_, first, ...rest) => {
  125. this.off(event, handler);
  126. if (listener) {
  127. listener.apply(null, [event, first, ...rest]);
  128. }
  129. deferred.resolve(first);
  130. };
  131. handler._originalListener = listener;
  132. this.on(event, handler);
  133. return deferred.promise;
  134. },
  135. /**
  136. * Remove a previously-registered event listener. Works for events
  137. * registered with either on or once.
  138. *
  139. * @param string event
  140. * The event name whose listener we're disconnecting.
  141. * @param function listener
  142. * The listener to remove.
  143. */
  144. off(event, listener) {
  145. if (!this._eventEmitterListeners) {
  146. return;
  147. }
  148. let listeners = this._eventEmitterListeners.get(event);
  149. if (listeners) {
  150. this._eventEmitterListeners.set(event, listeners.filter(l => {
  151. return l !== listener && l._originalListener !== listener;
  152. }));
  153. }
  154. },
  155. /**
  156. * Emit an event. All arguments to this method will
  157. * be sent to listener functions.
  158. */
  159. emit(event) {
  160. this.logEvent(event, arguments);
  161. if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(event)) {
  162. return;
  163. }
  164. let originalListeners = this._eventEmitterListeners.get(event);
  165. for (let listener of this._eventEmitterListeners.get(event)) {
  166. // If the object was destroyed during event emission, stop
  167. // emitting.
  168. if (!this._eventEmitterListeners) {
  169. break;
  170. }
  171. // If listeners were removed during emission, make sure the
  172. // event handler we're going to fire wasn't removed.
  173. if (originalListeners === this._eventEmitterListeners.get(event) ||
  174. this._eventEmitterListeners.get(event).some(l => l === listener)) {
  175. try {
  176. listener.apply(null, arguments);
  177. } catch (ex) {
  178. // Prevent a bad listener from interfering with the others.
  179. let msg = ex + ": " + ex.stack;
  180. console.error(msg);
  181. dump(msg + "\n");
  182. }
  183. }
  184. }
  185. },
  186. logEvent(event, args) {
  187. if (!loggingEnabled) {
  188. return;
  189. }
  190. let description = describeNthCaller(2);
  191. let argOut = "(";
  192. if (args.length === 1) {
  193. argOut += event;
  194. }
  195. let out = "EMITTING: ";
  196. // We need this try / catch to prevent any dead object errors.
  197. try {
  198. for (let i = 1; i < args.length; i++) {
  199. if (i === 1) {
  200. argOut = "(" + event + ", ";
  201. } else {
  202. argOut += ", ";
  203. }
  204. let arg = args[i];
  205. argOut += arg;
  206. if (arg && arg.nodeName) {
  207. argOut += " (" + arg.nodeName;
  208. if (arg.id) {
  209. argOut += "#" + arg.id;
  210. }
  211. if (arg.className) {
  212. argOut += "." + arg.className;
  213. }
  214. argOut += ")";
  215. }
  216. }
  217. } catch (e) {
  218. // Object is dead so the toolbox is most likely shutting down,
  219. // do nothing.
  220. }
  221. argOut += ")";
  222. out += "emit" + argOut + " from " + description + "\n";
  223. dump(out);
  224. },
  225. };
  226. });