// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Defines a handful of utility functions to simplify working
* with promises.
*/
'use strict'
const { isObject, isPromise } = require('./util')
/**
* Creates a promise that will be resolved at a set time in the future.
* @param {number} ms The amount of time, in milliseconds, to wait before
* resolving the promise.
* @return {!Promise<void>} The promise.
*/
function delayed(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* Wraps a function that expects a node-style callback as its final
* argument. This callback expects two arguments: an error value (which will be
* null if the call succeeded), and the success value as the second argument.
* The callback will the resolve or reject the returned promise, based on its
* arguments.
* @param {!Function} fn The function to wrap.
* @param {...?} args The arguments to apply to the function, excluding the
* final callback.
* @return {!Thenable} A promise that will be resolved with the
* result of the provided function's callback.
*/
function checkedNodeCall(fn, ...args) {
return new Promise(function (fulfill, reject) {
try {
fn(...args, function (error, value) {
error ? reject(error) : fulfill(value)
})
} catch (ex) {
reject(ex)
}
})
}
/**
* Registers a listener to invoke when a promise is resolved, regardless
* of whether the promise's value was successfully computed. This function
* is synonymous with the {@code finally} clause in a synchronous API:
*
* // Synchronous API:
* try {
* doSynchronousWork();
* } finally {
* cleanUp();
* }
*
* // Asynchronous promise API:
* doAsynchronousWork().finally(cleanUp);
*
* __Note:__ similar to the {@code finally} clause, if the registered
* callback returns a rejected promise or throws an error, it will silently
* replace the rejection error (if any) from this promise:
*
* try {
* throw Error('one');
* } finally {
* throw Error('two'); // Hides Error: one
* }
*
* let p = Promise.reject(Error('one'));
* promise.finally(p, function() {
* throw Error('two'); // Hides Error: one
* });
*
* @param {!IThenable<?>} promise The promise to add the listener to.
* @param {function(): (R|IThenable<R>)} callback The function to call when
* the promise is resolved.
* @return {!Promise<R>} A promise that will be resolved with the callback
* result.
* @template R
*/
async function thenFinally(promise, callback) {
try {
await Promise.resolve(promise)
return callback()
} catch (e) {
await callback()
throw e
}
}
/**
* Calls a function for each element in an array and inserts the result into a
* new array, which is used as the fulfillment value of the promise returned
* by this function.
*
* If the return value of the mapping function is a promise, this function
* will wait for it to be fulfilled before inserting it into the new array.
*
* If the mapping function throws or returns a rejected promise, the
* promise returned by this function will be rejected with the same reason.
* Only the first failure will be reported; all subsequent errors will be
* silently ignored.
*
* @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate
* over, or a promise that will resolve to said array.
* @param {function(this: SELF, TYPE, number, !Array<TYPE>): ?} fn The
* function to call for each element in the array. This function should
* expect three arguments (the element, the index, and the array itself.
* @param {SELF=} self The object to be used as the value of 'this' within `fn`.
* @template TYPE, SELF
*/
async function map(array, fn, self = undefined) {
const v = await Promise.resolve(array)
if (!Array.isArray(v)) {
throw TypeError('not an array')
}
const arr = /** @type {!Array} */ (v)
const values = []
for (const [index, item] of arr.entries()) {
values.push(await Promise.resolve(fn.call(self, item, index, arr)))
}
return values
}
/**
* Calls a function for each element in an array, and if the function returns
* true adds the element to a new array.
*
* If the return value of the filter function is a promise, this function
* will wait for it to be fulfilled before determining whether to insert the
* element into the new array.
*
* If the filter function throws or returns a rejected promise, the promise
* returned by this function will be rejected with the same reason. Only the
* first failure will be reported; all subsequent errors will be silently
* ignored.
*
* @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate
* over, or a promise that will resolve to said array.
* @param {function(this: SELF, TYPE, number, !Array<TYPE>): (
* boolean|IThenable<boolean>)} fn The function
* to call for each element in the array.
* @param {SELF=} self The object to be used as the value of 'this' within `fn`.
* @template TYPE, SELF
*/
async function filter(array, fn, self = undefined) {
const v = await Promise.resolve(array)
if (!Array.isArray(v)) {
throw TypeError('not an array')
}
const arr = /** @type {!Array} */ (v)
const values = []
for (const [index, item] of arr.entries()) {
const isConditionTrue = await Promise.resolve(fn.call(self, item, index, arr))
if (isConditionTrue) {
values.push(item)
}
}
return values
}
/**
* Returns a promise that will be resolved with the input value in a
* fully-resolved state. If the value is an array, each element will be fully
* resolved. Likewise, if the value is an object, all keys will be fully
* resolved. In both cases, all nested arrays and objects will also be
* fully resolved. All fields are resolved in place; the returned promise will
* resolve on {@code value} and not a copy.
*
* Warning: This function makes no checks against objects that contain
* cyclical references:
*
* var value = {};
* value['self'] = value;
* promise.fullyResolved(value); // Stack overflow.
*
* @param {*} value The value to fully resolve.
* @return {!Thenable} A promise for a fully resolved version
* of the input value.
*/
async function fullyResolved(value) {
value = await Promise.resolve(value)
if (Array.isArray(value)) {
return fullyResolveKeys(/** @type {!Array} */ (value))
}
if (isObject(value)) {
return fullyResolveKeys(/** @type {!Object} */ (value))
}
if (typeof value === 'function') {
return fullyResolveKeys(/** @type {!Object} */ (value))
}
return value
}
/**
* @param {!(Array|Object)} obj the object to resolve.
* @return {!Thenable} A promise that will be resolved with the
* input object once all of its values have been fully resolved.
*/
async function fullyResolveKeys(obj) {
const isArray = Array.isArray(obj)
const numKeys = isArray ? obj.length : Object.keys(obj).length
if (!numKeys) {
return obj
}
async function forEachProperty(obj, fn) {
for (let key in obj) {
await fn(obj[key], key)
}
}
async function forEachElement(arr, fn) {
for (let i = 0; i < arr.length; i++) {
await fn(arr[i], i)
}
}
const forEachKey = isArray ? forEachElement : forEachProperty
await forEachKey(obj, async function (partialValue, key) {
if (!Array.isArray(partialValue) && (!partialValue || typeof partialValue !== 'object')) {
return
}
obj[key] = await fullyResolved(partialValue)
})
return obj
}
// PUBLIC API
module.exports = {
checkedNodeCall,
delayed,
filter,
finally: thenFinally,
fullyResolved,
isPromise,
map,
}