ThreadSafeDevToolsUtils.js (9812B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /** 8 * General utilities used throughout devtools that can also be used in 9 * workers. 10 */ 11 12 /** 13 * Immutably reduce the given `...objs` into one object. The reduction is 14 * applied from left to right, so `immutableUpdate({ a: 1 }, { a: 2 })` will 15 * result in `{ a: 2 }`. The resulting object is frozen. 16 * 17 * Example usage: 18 * 19 * const original = { foo: 1, bar: 2, baz: 3 }; 20 * const modified = immutableUpdate(original, { baz: 0, bang: 4 }); 21 * 22 * // We get the new object that we expect... 23 * assert(modified.baz === 0); 24 * assert(modified.bang === 4); 25 * 26 * // However, the original is not modified. 27 * assert(original.baz === 2); 28 * assert(original.bang === undefined); 29 * 30 * @param {...object} ...objs 31 * @returns {object} 32 */ 33 exports.immutableUpdate = function (...objs) { 34 return Object.freeze(Object.assign({}, ...objs)); 35 }; 36 37 /** 38 * Utility function for updating an object with the properties of 39 * other objects. 40 * 41 * DEPRECATED: Just use Object.assign() instead! 42 * 43 * @param aTarget Object 44 * The object being updated. 45 * @param aNewAttrs Object 46 * The rest params are objects to update aTarget with. You 47 * can pass as many as you like. 48 */ 49 exports.update = function update(target, ...args) { 50 for (const attrs of args) { 51 for (const key in attrs) { 52 const desc = Object.getOwnPropertyDescriptor(attrs, key); 53 54 if (desc) { 55 Object.defineProperty(target, key, desc); 56 } 57 } 58 } 59 return target; 60 }; 61 62 /** 63 * Utility function for getting the values from an object as an array 64 * 65 * @param object Object 66 * The object to iterate over 67 */ 68 exports.values = function values(object) { 69 return Object.keys(object).map(k => object[k]); 70 }; 71 72 /** 73 * Report that |who| threw an exception, |exception|. 74 */ 75 exports.reportException = function reportException(who, exception) { 76 const msg = `${who} threw an exception: ${exports.safeErrorString( 77 exception 78 )}`; 79 dump(msg + "\n"); 80 81 if (typeof console !== "undefined" && console && console.error) { 82 console.error(exception); 83 } 84 }; 85 86 /** 87 * Given a handler function that may throw, return an infallible handler 88 * function that calls the fallible handler, and logs any exceptions it 89 * throws. 90 * 91 * @param handler function 92 * A handler function, which may throw. 93 * @param aName string 94 * A name for handler, for use in error messages. If omitted, we use 95 * handler.name. 96 * 97 * (SpiderMonkey does generate good names for anonymous functions, but we 98 * don't have a way to get at them from JavaScript at the moment.) 99 */ 100 exports.makeInfallible = function (handler, name = handler.name) { 101 return function () { 102 try { 103 return handler.apply(this, arguments); 104 } catch (ex) { 105 let who = "Handler function"; 106 if (name) { 107 who += " " + name; 108 } 109 exports.reportException(who, ex); 110 return undefined; 111 } 112 }; 113 }; 114 115 /** 116 * Turn the |error| into a string, without fail. 117 * 118 * @param {Error|any} error 119 */ 120 exports.safeErrorString = function (error) { 121 try { 122 let errorString = error.toString(); 123 if (typeof errorString == "string") { 124 // Attempt to attach a stack to |errorString|. If it throws an error, or 125 // isn't a string, don't use it. 126 try { 127 if (error.stack) { 128 const stack = error.stack.toString(); 129 if (typeof stack == "string") { 130 errorString += "\nStack: " + stack; 131 } 132 } 133 } catch (ee) { 134 // Ignore. 135 } 136 137 // Append additional line and column number information to the output, 138 // since it might not be part of the stringified error. 139 if ( 140 typeof error.lineNumber == "number" && 141 typeof error.columnNumber == "number" 142 ) { 143 errorString += 144 "Line: " + error.lineNumber + ", column: " + error.columnNumber; 145 } 146 147 return errorString; 148 } 149 } catch (ee) { 150 // Ignore. 151 } 152 153 // We failed to find a good error description, so do the next best thing. 154 return Object.prototype.toString.call(error); 155 }; 156 157 /** 158 * Interleaves two arrays element by element, returning the combined array, like 159 * a zip. In the case of arrays with different sizes, undefined values will be 160 * interleaved at the end along with the extra values of the larger array. 161 * 162 * @param Array a 163 * @param Array b 164 * @returns Array 165 * The combined array, in the form [a1, b1, a2, b2, ...] 166 */ 167 exports.zip = function (a, b) { 168 if (!b) { 169 return a; 170 } 171 if (!a) { 172 return b; 173 } 174 const pairs = []; 175 for ( 176 let i = 0, aLength = a.length, bLength = b.length; 177 i < aLength || i < bLength; 178 i++ 179 ) { 180 pairs.push([a[i], b[i]]); 181 } 182 return pairs; 183 }; 184 185 /** 186 * Converts an object into an array with 2-element arrays as key/value 187 * pairs of the object. `{ foo: 1, bar: 2}` would become 188 * `[[foo, 1], [bar 2]]` (order not guaranteed). 189 * 190 * @param object obj 191 * @returns array 192 */ 193 exports.entries = function entries(obj) { 194 return Object.keys(obj).map(k => [k, obj[k]]); 195 }; 196 197 /* 198 * Takes an array of 2-element arrays as key/values pairs and 199 * constructs an object using them. 200 */ 201 exports.toObject = function (arr) { 202 const obj = {}; 203 for (const [k, v] of arr) { 204 obj[k] = v; 205 } 206 return obj; 207 }; 208 209 /** 210 * Composes the given functions into a single function, which will 211 * apply the results of each function right-to-left, starting with 212 * applying the given arguments to the right-most function. 213 * `compose(foo, bar, baz)` === `args => foo(bar(baz(args)))` 214 * 215 * @param ...function funcs 216 * @returns function 217 */ 218 exports.compose = function compose(...funcs) { 219 return (...args) => { 220 const initialValue = funcs[funcs.length - 1](...args); 221 const leftFuncs = funcs.slice(0, -1); 222 return leftFuncs.reduceRight((composed, f) => f(composed), initialValue); 223 }; 224 }; 225 226 /** 227 * Return true if `thing` is a generator function, false otherwise. 228 */ 229 exports.isGenerator = function (fn) { 230 if (typeof fn !== "function") { 231 return false; 232 } 233 const proto = Object.getPrototypeOf(fn); 234 if (!proto) { 235 return false; 236 } 237 const ctor = proto.constructor; 238 if (!ctor) { 239 return false; 240 } 241 return ctor.name == "GeneratorFunction"; 242 }; 243 244 /** 245 * Return true if `thing` is an async function, false otherwise. 246 */ 247 exports.isAsyncFunction = function (fn) { 248 if (typeof fn !== "function") { 249 return false; 250 } 251 const proto = Object.getPrototypeOf(fn); 252 if (!proto) { 253 return false; 254 } 255 const ctor = proto.constructor; 256 if (!ctor) { 257 return false; 258 } 259 return ctor.name == "AsyncFunction"; 260 }; 261 262 /** 263 * Return true if `thing` is a Promise or then-able, false otherwise. 264 */ 265 exports.isPromise = function (p) { 266 return p && typeof p.then === "function"; 267 }; 268 269 /** 270 * Return true if `thing` is a SavedFrame, false otherwise. 271 */ 272 exports.isSavedFrame = function (thing) { 273 return Object.prototype.toString.call(thing) === "[object SavedFrame]"; 274 }; 275 276 /** 277 * Return true iff `thing` is a `Set` object (possibly from another global). 278 */ 279 exports.isSet = function (thing) { 280 return Object.prototype.toString.call(thing) === "[object Set]"; 281 }; 282 283 /** 284 * Given a list of lists, flatten it. Only flattens one level; does not 285 * recursively flatten all levels. 286 * 287 * @param {Array<Array<Any>>} lists 288 * @return {Array<Any>} 289 */ 290 exports.flatten = function (lists) { 291 return Array.prototype.concat.apply([], lists); 292 }; 293 294 /** 295 * Returns a promise that is resolved or rejected when all promises have settled 296 * (resolved or rejected). 297 * 298 * This differs from Promise.all, which will reject immediately after the first 299 * rejection, instead of waiting for the remaining promises to settle. 300 * 301 * @param values 302 * Iterable of promises that may be pending, resolved, or rejected. When 303 * when all promises have settled (resolved or rejected), the returned 304 * promise will be resolved or rejected as well. 305 * 306 * @return A new promise that is fulfilled when all values have settled 307 * (resolved or rejected). Its resolution value will be an array of all 308 * resolved values in the given order, or undefined if values is an 309 * empty array. The reject reason will be forwarded from the first 310 * promise in the list of given promises to be rejected. 311 */ 312 exports.settleAll = values => { 313 if (values === null || typeof values[Symbol.iterator] != "function") { 314 throw new Error("settleAll() expects an iterable."); 315 } 316 317 return new Promise((resolve, reject) => { 318 values = Array.isArray(values) ? values : [...values]; 319 let countdown = values.length; 320 const resolutionValues = new Array(countdown); 321 let rejectionValue; 322 let rejectionOccurred = false; 323 324 if (!countdown) { 325 resolve(resolutionValues); 326 return; 327 } 328 329 function checkForCompletion() { 330 if (--countdown > 0) { 331 return; 332 } 333 if (!rejectionOccurred) { 334 resolve(resolutionValues); 335 } else { 336 reject(rejectionValue); 337 } 338 } 339 340 for (let i = 0; i < values.length; i++) { 341 const index = i; 342 const value = values[i]; 343 const resolver = result => { 344 resolutionValues[index] = result; 345 checkForCompletion(); 346 }; 347 const rejecter = error => { 348 if (!rejectionOccurred) { 349 rejectionValue = error; 350 rejectionOccurred = true; 351 } 352 checkForCompletion(); 353 }; 354 355 if (value && typeof value.then == "function") { 356 value.then(resolver, rejecter); 357 } else { 358 // Given value is not a promise, forward it as a resolution value. 359 resolver(value); 360 } 361 } 362 }); 363 };