base.js revision 6e8cce623b6e4fe0c9e4af605d675dd9d0338c38
1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview
7 * A module that contains basic utility components and methods for the
8 * chromoting project
9 *
10 */
11
12'use strict';
13
14var base = {};
15base.debug = function() {};
16
17/**
18 * Whether to break in debugger and alert when an assertion fails.
19 * Set it to true for debugging.
20 * @type {boolean}
21 */
22base.debug.breakOnAssert = false;
23
24/**
25 * Assert that |expr| is true else print the |opt_msg|.
26 * @param {boolean} expr
27 * @param {string=} opt_msg
28 */
29base.debug.assert = function(expr, opt_msg) {
30  if (!expr) {
31    var msg = 'Assertion Failed.';
32    if (opt_msg) {
33      msg += ' ' + opt_msg;
34    }
35    console.error(msg);
36    if (base.debug.breakOnAssert) {
37      alert(msg);
38      debugger;
39    }
40  }
41};
42
43/**
44 * @return {string} The callstack of the current method.
45 */
46base.debug.callstack = function() {
47  try {
48    throw new Error();
49  } catch (e) {
50    var error = /** @type {Error} */ e;
51    var callstack = error.stack
52      .replace(/^\s+(at eval )?at\s+/gm, '') // Remove 'at' and indentation.
53      .split('\n');
54    callstack.splice(0,2); // Remove the stack of the current function.
55  }
56  return callstack.join('\n');
57};
58
59/**
60  * @interface
61  */
62base.Disposable = function() {};
63base.Disposable.prototype.dispose = function() {};
64
65/**
66 * A utility function to invoke |obj|.dispose without a null check on |obj|.
67 * @param {base.Disposable} obj
68 */
69base.dispose = function(obj) {
70  if (obj) {
71    base.debug.assert(typeof obj.dispose == 'function');
72    obj.dispose();
73  }
74};
75
76/**
77 * Copy all properties from src to dest.
78 * @param {Object} dest
79 * @param {Object} src
80 */
81base.mix = function(dest, src) {
82  for (var prop in src) {
83    if (src.hasOwnProperty(prop)) {
84      base.debug.assert(!dest.hasOwnProperty(prop),"Don't override properties");
85      dest[prop] = src[prop];
86    }
87  }
88};
89
90/**
91 * Adds a mixin to a class.
92 * @param {Object} dest
93 * @param {Object} src
94 * @suppress {checkTypes}
95 */
96base.extend = function(dest, src) {
97  base.mix(dest.prototype, src.prototype || src);
98};
99
100base.doNothing = function() {};
101
102/**
103 * Returns an array containing the values of |dict|.
104 * @param {!Object} dict
105 * @return {Array}
106 */
107base.values = function(dict) {
108  return Object.keys(dict).map(
109    /** @param {string} key */
110    function(key) {
111      return dict[key];
112    });
113};
114
115
116/**
117 * Joins the |url| with optional query parameters defined in |opt_params|
118 * See unit test for usage.
119 * @param {string} url
120 * @param {Object.<string>=} opt_params
121 * @return {string}
122 */
123base.urlJoin = function(url, opt_params) {
124  if (!opt_params) {
125    return url;
126  }
127  var queryParameters = [];
128  for (var key in opt_params) {
129    queryParameters.push(encodeURIComponent(key) + "=" +
130                         encodeURIComponent(opt_params[key]));
131  }
132  return url + '?' + queryParameters.join('&');
133};
134
135base.Promise = function() {};
136
137/**
138 * @param {number} delay
139 * @return {Promise} a Promise that will be fulfilled after |delay| ms.
140 */
141base.Promise.sleep = function(delay) {
142  return new Promise(
143    /** @param {function():void} fulfill */
144    function(fulfill) {
145      window.setTimeout(fulfill, delay);
146    });
147};
148
149
150/**
151 * @param {Promise} promise
152 * @return {Promise} a Promise that will be fulfilled iff the specified Promise
153 *     is rejected.
154 */
155base.Promise.negate = function(promise) {
156  return promise.then(
157      /** @return {Promise} */
158      function() {
159        return Promise.reject();
160      },
161      /** @return {Promise} */
162      function() {
163        return Promise.resolve();
164      });
165};
166
167/**
168 * A mixin for classes with events.
169 *
170 * For example, to create an alarm event for SmokeDetector:
171 * functionSmokeDetector() {
172 *    this.defineEvents(['alarm']);
173 * };
174 * base.extend(SmokeDetector, base.EventSource);
175 *
176 * To fire an event:
177 * SmokeDetector.prototype.onCarbonMonoxideDetected = function() {
178 *   var param = {} // optional parameters
179 *   this.raiseEvent('alarm', param);
180 * }
181 *
182 * To listen to an event:
183 * var smokeDetector = new SmokeDetector();
184 * smokeDetector.addEventListener('alarm', listenerObj.someCallback)
185 *
186 */
187
188/**
189  * Helper interface for the EventSource.
190  * @constructor
191  */
192base.EventEntry = function() {
193  /** @type {Array.<function():void>} */
194  this.listeners = [];
195};
196
197/**
198  * @constructor
199  * Since this class is implemented as a mixin, the constructor may not be
200  * called.  All initializations should be done in defineEvents.
201  */
202base.EventSource = function() {
203  /** @type {Object.<string, base.EventEntry>} */
204  this.eventMap_;
205};
206
207/**
208  * @param {base.EventSource} obj
209  * @param {string} type
210  */
211base.EventSource.isDefined = function(obj, type) {
212  base.debug.assert(Boolean(obj.eventMap_),
213                   "The object doesn't support events");
214  base.debug.assert(Boolean(obj.eventMap_[type]), 'Event <' + type +
215    '> is undefined for the current object');
216};
217
218base.EventSource.prototype = {
219  /**
220    * Define |events| for this event source.
221    * @param {Array.<string>} events
222    */
223  defineEvents: function(events) {
224    base.debug.assert(!Boolean(this.eventMap_),
225                     'defineEvents can only be called once.');
226    this.eventMap_ = {};
227    events.forEach(
228      /**
229        * @this {base.EventSource}
230        * @param {string} type
231        */
232      function(type) {
233        base.debug.assert(typeof type == 'string');
234        this.eventMap_[type] = new base.EventEntry();
235    }, this);
236  },
237
238  /**
239    * Add a listener |fn| to listen to |type| event.
240    * @param {string} type
241    * @param {function(?=):void} fn
242    */
243  addEventListener: function(type, fn) {
244    base.debug.assert(typeof fn == 'function');
245    base.EventSource.isDefined(this, type);
246
247    var listeners = this.eventMap_[type].listeners;
248    listeners.push(fn);
249  },
250
251  /**
252    * Remove the listener |fn| from the event source.
253    * @param {string} type
254    * @param {function(?=):void} fn
255    */
256  removeEventListener: function(type, fn) {
257    base.debug.assert(typeof fn == 'function');
258    base.EventSource.isDefined(this, type);
259
260    var listeners = this.eventMap_[type].listeners;
261    // find the listener to remove.
262    for (var i = 0; i < listeners.length; i++) {
263      var listener = listeners[i];
264      if (listener == fn) {
265        listeners.splice(i, 1);
266        break;
267      }
268    }
269  },
270
271  /**
272    * Fire an event of a particular type on this object.
273    * @param {string} type
274    * @param {*=} opt_details The type of |opt_details| should be ?= to
275    *     match what is defined in add(remove)EventListener.  However, JSCompile
276    *     cannot handle invoking an unknown type as an argument to |listener|
277    *     As a hack, we set the type to *=.
278    */
279  raiseEvent: function(type, opt_details) {
280    base.EventSource.isDefined(this, type);
281
282    var entry = this.eventMap_[type];
283    var listeners = entry.listeners.slice(0); // Make a copy of the listeners.
284
285    listeners.forEach(
286      /** @param {function(*=):void} listener */
287      function(listener){
288        if (listener) {
289          listener(opt_details);
290        }
291    });
292  }
293};
294