blob: ee909bede327c38f7cac29fcca6f379d5c7ea42e [file] [log] [blame]
// 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))));
}