base.js revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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 * @type {boolean|undefined}
117 * @private
118 */
119base.isAppsV2_ = undefined;
120
121/**
122 * @return {boolean} True if this is a v2 app; false if it is a legacy app.
123 */
124base.isAppsV2 = function() {
125  if (base.isAppsV2_ === undefined) {
126    var manifest = chrome.runtime.getManifest();
127    base.isAppsV2_ =
128        Boolean(manifest && manifest.app && manifest.app.background);
129  }
130  return base.isAppsV2_;
131};
132
133/**
134 * Joins the |url| with optional query parameters defined in |opt_params|
135 * See unit test for usage.
136 * @param {string} url
137 * @param {Object.<string>=} opt_params
138 * @return {string}
139 */
140base.urlJoin = function(url, opt_params) {
141  if (!opt_params) {
142    return url;
143  }
144  var queryParameters = [];
145  for (var key in opt_params) {
146    queryParameters.push(encodeURIComponent(key) + "=" +
147                         encodeURIComponent(opt_params[key]));
148  }
149  return url + '?' + queryParameters.join('&');
150};
151
152/**
153 * Convert special characters (e.g. &, < and >) to HTML entities.
154 *
155 * @param {string} str
156 * @return {string}
157 */
158base.escapeHTML = function(str) {
159  var div = document.createElement('div');
160  div.appendChild(document.createTextNode(str));
161  return div.innerHTML;
162};
163
164/**
165 * Promise is a great tool for writing asynchronous code. However, the construct
166 *   var p = new promise(function init(resolve, reject) {
167 *     ... // code that fulfills the Promise.
168 *   });
169 * forces the Promise-resolving logic to reside in the |init| function
170 * of the constructor.  This is problematic when you need to resolve the
171 * Promise in a member function(which is quite common for event callbacks).
172 *
173 * base.Deferred comes to the rescue.  It encapsulates a Promise
174 * object and exposes member methods (resolve/reject) to fulfill it.
175 *
176 * Here are the recommended steps to follow when implementing an asynchronous
177 * function that returns a Promise:
178 * 1. Create a deferred object by calling
179 *      var deferred = new base.Deferred();
180 * 2. Call deferred.resolve() when the asynchronous operation finishes.
181 * 3. Call deferred.reject() when the asynchronous operation fails.
182 * 4. Return deferred.promise() to the caller so that it can subscribe
183 *    to status changes using the |then| handler.
184 *
185 * Sample Usage:
186 *  function myAsyncAPI() {
187 *    var deferred = new base.Deferred();
188 *    window.setTimeout(function() {
189 *      deferred.resolve();
190 *    }, 100);
191 *    return deferred.promise();
192 *  };
193 *
194 * @constructor
195 */
196base.Deferred = function() {
197  /**
198   * @type {?function(?=)}
199   * @private
200   */
201  this.resolve_ = null;
202
203  /**
204   * @type {?function(?)}
205   * @private
206   */
207  this.reject_ = null;
208
209  /**
210   * @type {Promise}
211   * @private
212   */
213  this.promise_ = new Promise(
214    /**
215     * @param {function(?=):void} resolve
216     * @param {function(?):void} reject
217     * @this {base.Deferred}
218     */
219    function(resolve, reject) {
220      this.resolve_ = resolve;
221      this.reject_ = reject;
222    }.bind(this)
223  );
224};
225
226/** @param {*} reason */
227base.Deferred.prototype.reject = function(reason) {
228  this.reject_(reason);
229};
230
231/** @param {*=} opt_value */
232base.Deferred.prototype.resolve = function(opt_value) {
233  this.resolve_(opt_value);
234};
235
236/** @return {Promise} */
237base.Deferred.prototype.promise = function() {
238  return this.promise_;
239};
240
241base.Promise = function() {};
242
243/**
244 * @param {number} delay
245 * @return {Promise} a Promise that will be fulfilled after |delay| ms.
246 */
247base.Promise.sleep = function(delay) {
248  return new Promise(
249    /** @param {function():void} fulfill */
250    function(fulfill) {
251      window.setTimeout(fulfill, delay);
252    });
253};
254
255
256/**
257 * @param {Promise} promise
258 * @return {Promise} a Promise that will be fulfilled iff the specified Promise
259 *     is rejected.
260 */
261base.Promise.negate = function(promise) {
262  return promise.then(
263      /** @return {Promise} */
264      function() {
265        return Promise.reject();
266      },
267      /** @return {Promise} */
268      function() {
269        return Promise.resolve();
270      });
271};
272
273/**
274 * A mixin for classes with events.
275 *
276 * For example, to create an alarm event for SmokeDetector:
277 * functionSmokeDetector() {
278 *    this.defineEvents(['alarm']);
279 * };
280 * base.extend(SmokeDetector, base.EventSource);
281 *
282 * To fire an event:
283 * SmokeDetector.prototype.onCarbonMonoxideDetected = function() {
284 *   var param = {} // optional parameters
285 *   this.raiseEvent('alarm', param);
286 * }
287 *
288 * To listen to an event:
289 * var smokeDetector = new SmokeDetector();
290 * smokeDetector.addEventListener('alarm', listenerObj.someCallback)
291 *
292 */
293
294/**
295  * Helper interface for the EventSource.
296  * @constructor
297  */
298base.EventEntry = function() {
299  /** @type {Array.<function():void>} */
300  this.listeners = [];
301};
302
303/**
304  * @constructor
305  * Since this class is implemented as a mixin, the constructor may not be
306  * called.  All initializations should be done in defineEvents.
307  */
308base.EventSource = function() {
309  /** @type {Object.<string, base.EventEntry>} */
310  this.eventMap_;
311};
312
313/**
314  * @param {base.EventSource} obj
315  * @param {string} type
316  */
317base.EventSource.isDefined = function(obj, type) {
318  base.debug.assert(Boolean(obj.eventMap_),
319                   "The object doesn't support events");
320  base.debug.assert(Boolean(obj.eventMap_[type]), 'Event <' + type +
321    '> is undefined for the current object');
322};
323
324base.EventSource.prototype = {
325  /**
326    * Define |events| for this event source.
327    * @param {Array.<string>} events
328    */
329  defineEvents: function(events) {
330    base.debug.assert(!Boolean(this.eventMap_),
331                     'defineEvents can only be called once.');
332    this.eventMap_ = {};
333    events.forEach(
334      /**
335        * @this {base.EventSource}
336        * @param {string} type
337        */
338      function(type) {
339        base.debug.assert(typeof type == 'string');
340        this.eventMap_[type] = new base.EventEntry();
341    }, this);
342  },
343
344  /**
345    * Add a listener |fn| to listen to |type| event.
346    * @param {string} type
347    * @param {function(?=):void} fn
348    */
349  addEventListener: function(type, fn) {
350    base.debug.assert(typeof fn == 'function');
351    base.EventSource.isDefined(this, type);
352
353    var listeners = this.eventMap_[type].listeners;
354    listeners.push(fn);
355  },
356
357  /**
358    * Remove the listener |fn| from the event source.
359    * @param {string} type
360    * @param {function(?=):void} fn
361    */
362  removeEventListener: function(type, fn) {
363    base.debug.assert(typeof fn == 'function');
364    base.EventSource.isDefined(this, type);
365
366    var listeners = this.eventMap_[type].listeners;
367    // find the listener to remove.
368    for (var i = 0; i < listeners.length; i++) {
369      var listener = listeners[i];
370      if (listener == fn) {
371        listeners.splice(i, 1);
372        break;
373      }
374    }
375  },
376
377  /**
378    * Fire an event of a particular type on this object.
379    * @param {string} type
380    * @param {*=} opt_details The type of |opt_details| should be ?= to
381    *     match what is defined in add(remove)EventListener.  However, JSCompile
382    *     cannot handle invoking an unknown type as an argument to |listener|
383    *     As a hack, we set the type to *=.
384    */
385  raiseEvent: function(type, opt_details) {
386    base.EventSource.isDefined(this, type);
387
388    var entry = this.eventMap_[type];
389    var listeners = entry.listeners.slice(0); // Make a copy of the listeners.
390
391    listeners.forEach(
392      /** @param {function(*=):void} listener */
393      function(listener){
394        if (listener) {
395          listener(opt_details);
396        }
397    });
398  }
399};
400
401/**
402  * Converts UTF-8 string to ArrayBuffer.
403  *
404  * @param {string} string
405  * @return {ArrayBuffer}
406  */
407base.encodeUtf8 = function(string) {
408  var utf8String = unescape(encodeURIComponent(string));
409  var result = new Uint8Array(utf8String.length);
410  for (var i = 0; i < utf8String.length; i++)
411    result[i] = utf8String.charCodeAt(i);
412  return result.buffer;
413}
414
415/**
416  * Decodes UTF-8 string from ArrayBuffer.
417  *
418  * @param {ArrayBuffer} buffer
419  * @return {string}
420  */
421base.decodeUtf8 = function(buffer) {
422  return decodeURIComponent(
423      escape(String.fromCharCode.apply(null, new Uint8Array(buffer))));
424}
425