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