Javascript  |  425行  |  10.33 KB

// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview
 * A module that contains basic utility components and methods for the
 * chromoting project
 *
 */

'use strict';

var base = {};
base.debug = function() {};

/**
 * Whether to break in debugger and alert when an assertion fails.
 * Set it to true for debugging.
 * @type {boolean}
 */
base.debug.breakOnAssert = false;

/**
 * Assert that |expr| is true else print the |opt_msg|.
 * @param {boolean} expr
 * @param {string=} opt_msg
 */
base.debug.assert = function(expr, opt_msg) {
  if (!expr) {
    var msg = 'Assertion Failed.';
    if (opt_msg) {
      msg += ' ' + opt_msg;
    }
    console.error(msg);
    if (base.debug.breakOnAssert) {
      alert(msg);
      debugger;
    }
  }
};

/**
 * @return {string} The callstack of the current method.
 */
base.debug.callstack = function() {
  try {
    throw new Error();
  } catch (e) {
    var error = /** @type {Error} */ e;
    var callstack = error.stack
      .replace(/^\s+(at eval )?at\s+/gm, '') // Remove 'at' and indentation.
      .split('\n');
    callstack.splice(0,2); // Remove the stack of the current function.
  }
  return callstack.join('\n');
};

/**
  * @interface
  */
base.Disposable = function() {};
base.Disposable.prototype.dispose = function() {};

/**
 * A utility function to invoke |obj|.dispose without a null check on |obj|.
 * @param {base.Disposable} obj
 */
base.dispose = function(obj) {
  if (obj) {
    base.debug.assert(typeof obj.dispose == 'function');
    obj.dispose();
  }
};

/**
 * Copy all properties from src to dest.
 * @param {Object} dest
 * @param {Object} src
 */
base.mix = function(dest, src) {
  for (var prop in src) {
    if (src.hasOwnProperty(prop)) {
      base.debug.assert(!dest.hasOwnProperty(prop),"Don't override properties");
      dest[prop] = src[prop];
    }
  }
};

/**
 * Adds a mixin to a class.
 * @param {Object} dest
 * @param {Object} src
 * @suppress {checkTypes}
 */
base.extend = function(dest, src) {
  base.mix(dest.prototype, src.prototype || src);
};

base.doNothing = function() {};

/**
 * Returns an array containing the values of |dict|.
 * @param {!Object} dict
 * @return {Array}
 */
base.values = function(dict) {
  return Object.keys(dict).map(
    /** @param {string} key */
    function(key) {
      return dict[key];
    });
};

/**
 * @type {boolean|undefined}
 * @private
 */
base.isAppsV2_ = undefined;

/**
 * @return {boolean} True if this is a v2 app; false if it is a legacy app.
 */
base.isAppsV2 = function() {
  if (base.isAppsV2_ === undefined) {
    var manifest = chrome.runtime.getManifest();
    base.isAppsV2_ =
        Boolean(manifest && manifest.app && manifest.app.background);
  }
  return base.isAppsV2_;
};

/**
 * Joins the |url| with optional query parameters defined in |opt_params|
 * See unit test for usage.
 * @param {string} url
 * @param {Object.<string>=} opt_params
 * @return {string}
 */
base.urlJoin = function(url, opt_params) {
  if (!opt_params) {
    return url;
  }
  var queryParameters = [];
  for (var key in opt_params) {
    queryParameters.push(encodeURIComponent(key) + "=" +
                         encodeURIComponent(opt_params[key]));
  }
  return url + '?' + queryParameters.join('&');
};

/**
 * Convert special characters (e.g. &, < and >) to HTML entities.
 *
 * @param {string} str
 * @return {string}
 */
base.escapeHTML = function(str) {
  var div = document.createElement('div');
  div.appendChild(document.createTextNode(str));
  return div.innerHTML;
};

/**
 * Promise is a great tool for writing asynchronous code. However, the construct
 *   var p = new promise(function init(resolve, reject) {
 *     ... // code that fulfills the Promise.
 *   });
 * forces the Promise-resolving logic to reside in the |init| function
 * of the constructor.  This is problematic when you need to resolve the
 * Promise in a member function(which is quite common for event callbacks).
 *
 * base.Deferred comes to the rescue.  It encapsulates a Promise
 * object and exposes member methods (resolve/reject) to fulfill it.
 *
 * Here are the recommended steps to follow when implementing an asynchronous
 * function that returns a Promise:
 * 1. Create a deferred object by calling
 *      var deferred = new base.Deferred();
 * 2. Call deferred.resolve() when the asynchronous operation finishes.
 * 3. Call deferred.reject() when the asynchronous operation fails.
 * 4. Return deferred.promise() to the caller so that it can subscribe
 *    to status changes using the |then| handler.
 *
 * Sample Usage:
 *  function myAsyncAPI() {
 *    var deferred = new base.Deferred();
 *    window.setTimeout(function() {
 *      deferred.resolve();
 *    }, 100);
 *    return deferred.promise();
 *  };
 *
 * @constructor
 */
base.Deferred = function() {
  /**
   * @type {?function(?=)}
   * @private
   */
  this.resolve_ = null;

  /**
   * @type {?function(?)}
   * @private
   */
  this.reject_ = null;

  /**
   * @type {Promise}
   * @private
   */
  this.promise_ = new Promise(
    /**
     * @param {function(?=):void} resolve
     * @param {function(?):void} reject
     * @this {base.Deferred}
     */
    function(resolve, reject) {
      this.resolve_ = resolve;
      this.reject_ = reject;
    }.bind(this)
  );
};

/** @param {*} reason */
base.Deferred.prototype.reject = function(reason) {
  this.reject_(reason);
};

/** @param {*=} opt_value */
base.Deferred.prototype.resolve = function(opt_value) {
  this.resolve_(opt_value);
};

/** @return {Promise} */
base.Deferred.prototype.promise = function() {
  return this.promise_;
};

base.Promise = function() {};

/**
 * @param {number} delay
 * @return {Promise} a Promise that will be fulfilled after |delay| ms.
 */
base.Promise.sleep = function(delay) {
  return new Promise(
    /** @param {function():void} fulfill */
    function(fulfill) {
      window.setTimeout(fulfill, delay);
    });
};


/**
 * @param {Promise} promise
 * @return {Promise} a Promise that will be fulfilled iff the specified Promise
 *     is rejected.
 */
base.Promise.negate = function(promise) {
  return promise.then(
      /** @return {Promise} */
      function() {
        return Promise.reject();
      },
      /** @return {Promise} */
      function() {
        return Promise.resolve();
      });
};

/**
 * A mixin for classes with events.
 *
 * For example, to create an alarm event for SmokeDetector:
 * functionSmokeDetector() {
 *    this.defineEvents(['alarm']);
 * };
 * base.extend(SmokeDetector, base.EventSource);
 *
 * To fire an event:
 * SmokeDetector.prototype.onCarbonMonoxideDetected = function() {
 *   var param = {} // optional parameters
 *   this.raiseEvent('alarm', param);
 * }
 *
 * To listen to an event:
 * var smokeDetector = new SmokeDetector();
 * smokeDetector.addEventListener('alarm', listenerObj.someCallback)
 *
 */

/**
  * Helper interface for the EventSource.
  * @constructor
  */
base.EventEntry = function() {
  /** @type {Array.<function():void>} */
  this.listeners = [];
};

/**
  * @constructor
  * Since this class is implemented as a mixin, the constructor may not be
  * called.  All initializations should be done in defineEvents.
  */
base.EventSource = function() {
  /** @type {Object.<string, base.EventEntry>} */
  this.eventMap_;
};

/**
  * @param {base.EventSource} obj
  * @param {string} type
  */
base.EventSource.isDefined = function(obj, type) {
  base.debug.assert(Boolean(obj.eventMap_),
                   "The object doesn't support events");
  base.debug.assert(Boolean(obj.eventMap_[type]), 'Event <' + type +
    '> is undefined for the current object');
};

base.EventSource.prototype = {
  /**
    * Define |events| for this event source.
    * @param {Array.<string>} events
    */
  defineEvents: function(events) {
    base.debug.assert(!Boolean(this.eventMap_),
                     'defineEvents can only be called once.');
    this.eventMap_ = {};
    events.forEach(
      /**
        * @this {base.EventSource}
        * @param {string} type
        */
      function(type) {
        base.debug.assert(typeof type == 'string');
        this.eventMap_[type] = new base.EventEntry();
    }, this);
  },

  /**
    * Add a listener |fn| to listen to |type| event.
    * @param {string} type
    * @param {function(?=):void} fn
    */
  addEventListener: function(type, fn) {
    base.debug.assert(typeof fn == 'function');
    base.EventSource.isDefined(this, type);

    var listeners = this.eventMap_[type].listeners;
    listeners.push(fn);
  },

  /**
    * Remove the listener |fn| from the event source.
    * @param {string} type
    * @param {function(?=):void} fn
    */
  removeEventListener: function(type, fn) {
    base.debug.assert(typeof fn == 'function');
    base.EventSource.isDefined(this, type);

    var listeners = this.eventMap_[type].listeners;
    // find the listener to remove.
    for (var i = 0; i < listeners.length; i++) {
      var listener = listeners[i];
      if (listener == fn) {
        listeners.splice(i, 1);
        break;
      }
    }
  },

  /**
    * Fire an event of a particular type on this object.
    * @param {string} type
    * @param {*=} opt_details The type of |opt_details| should be ?= to
    *     match what is defined in add(remove)EventListener.  However, JSCompile
    *     cannot handle invoking an unknown type as an argument to |listener|
    *     As a hack, we set the type to *=.
    */
  raiseEvent: function(type, opt_details) {
    base.EventSource.isDefined(this, type);

    var entry = this.eventMap_[type];
    var listeners = entry.listeners.slice(0); // Make a copy of the listeners.

    listeners.forEach(
      /** @param {function(*=):void} listener */
      function(listener){
        if (listener) {
          listener(opt_details);
        }
    });
  }
};

/**
  * Converts UTF-8 string to ArrayBuffer.
  *
  * @param {string} string
  * @return {ArrayBuffer}
  */
base.encodeUtf8 = function(string) {
  var utf8String = unescape(encodeURIComponent(string));
  var result = new Uint8Array(utf8String.length);
  for (var i = 0; i < utf8String.length; i++)
    result[i] = utf8String.charCodeAt(i);
  return result.buffer;
}

/**
  * Decodes UTF-8 string from ArrayBuffer.
  *
  * @param {ArrayBuffer} buffer
  * @return {string}
  */
base.decodeUtf8 = function(buffer) {
  return decodeURIComponent(
      escape(String.fromCharCode.apply(null, new Uint8Array(buffer))));
}