Path: blob/trunk/javascript/selenium-webdriver/lib/promise.js
2884 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617/**18* @fileoverview Defines a handful of utility functions to simplify working19* with promises.20*/2122'use strict'2324const { isObject, isPromise } = require('./util')2526/**27* Creates a promise that will be resolved at a set time in the future.28* @param {number} ms The amount of time, in milliseconds, to wait before29* resolving the promise.30* @return {!Promise<void>} The promise.31*/32function delayed(ms) {33return new Promise((resolve) => setTimeout(resolve, ms))34}3536/**37* Wraps a function that expects a node-style callback as its final38* argument. This callback expects two arguments: an error value (which will be39* null if the call succeeded), and the success value as the second argument.40* The callback will the resolve or reject the returned promise, based on its41* arguments.42* @param {!Function} fn The function to wrap.43* @param {...?} args The arguments to apply to the function, excluding the44* final callback.45* @return {!Thenable} A promise that will be resolved with the46* result of the provided function's callback.47*/48function checkedNodeCall(fn, ...args) {49return new Promise(function (fulfill, reject) {50try {51fn(...args, function (error, value) {52error ? reject(error) : fulfill(value)53})54} catch (ex) {55reject(ex)56}57})58}5960/**61* Registers a listener to invoke when a promise is resolved, regardless62* of whether the promise's value was successfully computed. This function63* is synonymous with the {@code finally} clause in a synchronous API:64*65* // Synchronous API:66* try {67* doSynchronousWork();68* } finally {69* cleanUp();70* }71*72* // Asynchronous promise API:73* doAsynchronousWork().finally(cleanUp);74*75* __Note:__ similar to the {@code finally} clause, if the registered76* callback returns a rejected promise or throws an error, it will silently77* replace the rejection error (if any) from this promise:78*79* try {80* throw Error('one');81* } finally {82* throw Error('two'); // Hides Error: one83* }84*85* let p = Promise.reject(Error('one'));86* promise.finally(p, function() {87* throw Error('two'); // Hides Error: one88* });89*90* @param {!IThenable<?>} promise The promise to add the listener to.91* @param {function(): (R|IThenable<R>)} callback The function to call when92* the promise is resolved.93* @return {!Promise<R>} A promise that will be resolved with the callback94* result.95* @template R96*/97async function thenFinally(promise, callback) {98try {99await Promise.resolve(promise)100return callback()101} catch (e) {102await callback()103throw e104}105}106107/**108* Calls a function for each element in an array and inserts the result into a109* new array, which is used as the fulfillment value of the promise returned110* by this function.111*112* If the return value of the mapping function is a promise, this function113* will wait for it to be fulfilled before inserting it into the new array.114*115* If the mapping function throws or returns a rejected promise, the116* promise returned by this function will be rejected with the same reason.117* Only the first failure will be reported; all subsequent errors will be118* silently ignored.119*120* @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate121* over, or a promise that will resolve to said array.122* @param {function(this: SELF, TYPE, number, !Array<TYPE>): ?} fn The123* function to call for each element in the array. This function should124* expect three arguments (the element, the index, and the array itself.125* @param {SELF=} self The object to be used as the value of 'this' within `fn`.126* @template TYPE, SELF127*/128async function map(array, fn, self = undefined) {129const v = await Promise.resolve(array)130if (!Array.isArray(v)) {131throw TypeError('not an array')132}133134const arr = /** @type {!Array} */ (v)135const values = []136137for (const [index, item] of arr.entries()) {138values.push(await Promise.resolve(fn.call(self, item, index, arr)))139}140141return values142}143144/**145* Calls a function for each element in an array, and if the function returns146* true adds the element to a new array.147*148* If the return value of the filter function is a promise, this function149* will wait for it to be fulfilled before determining whether to insert the150* element into the new array.151*152* If the filter function throws or returns a rejected promise, the promise153* returned by this function will be rejected with the same reason. Only the154* first failure will be reported; all subsequent errors will be silently155* ignored.156*157* @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate158* over, or a promise that will resolve to said array.159* @param {function(this: SELF, TYPE, number, !Array<TYPE>): (160* boolean|IThenable<boolean>)} fn The function161* to call for each element in the array.162* @param {SELF=} self The object to be used as the value of 'this' within `fn`.163* @template TYPE, SELF164*/165async function filter(array, fn, self = undefined) {166const v = await Promise.resolve(array)167if (!Array.isArray(v)) {168throw TypeError('not an array')169}170171const arr = /** @type {!Array} */ (v)172const values = []173174for (const [index, item] of arr.entries()) {175const isConditionTrue = await Promise.resolve(fn.call(self, item, index, arr))176if (isConditionTrue) {177values.push(item)178}179}180181return values182}183184/**185* Returns a promise that will be resolved with the input value in a186* fully-resolved state. If the value is an array, each element will be fully187* resolved. Likewise, if the value is an object, all keys will be fully188* resolved. In both cases, all nested arrays and objects will also be189* fully resolved. All fields are resolved in place; the returned promise will190* resolve on {@code value} and not a copy.191*192* Warning: This function makes no checks against objects that contain193* cyclical references:194*195* var value = {};196* value['self'] = value;197* promise.fullyResolved(value); // Stack overflow.198*199* @param {*} value The value to fully resolve.200* @return {!Thenable} A promise for a fully resolved version201* of the input value.202*/203async function fullyResolved(value) {204value = await Promise.resolve(value)205if (Array.isArray(value)) {206return fullyResolveKeys(/** @type {!Array} */ (value))207}208209if (isObject(value)) {210return fullyResolveKeys(/** @type {!Object} */ (value))211}212213if (typeof value === 'function') {214return fullyResolveKeys(/** @type {!Object} */ (value))215}216217return value218}219220/**221* @param {!(Array|Object)} obj the object to resolve.222* @return {!Thenable} A promise that will be resolved with the223* input object once all of its values have been fully resolved.224*/225async function fullyResolveKeys(obj) {226const isArray = Array.isArray(obj)227const numKeys = isArray ? obj.length : Object.keys(obj).length228229if (!numKeys) {230return obj231}232233async function forEachProperty(obj, fn) {234for (let key in obj) {235await fn(obj[key], key)236}237}238239async function forEachElement(arr, fn) {240for (let i = 0; i < arr.length; i++) {241await fn(arr[i], i)242}243}244245const forEachKey = isArray ? forEachElement : forEachProperty246await forEachKey(obj, async function (partialValue, key) {247if (!Array.isArray(partialValue) && (!partialValue || typeof partialValue !== 'object')) {248return249}250obj[key] = await fullyResolved(partialValue)251})252return obj253}254255// PUBLIC API256257module.exports = {258checkedNodeCall,259delayed,260filter,261finally: thenFinally,262fullyResolved,263isPromise,264map,265}266267268