UNPKG

9 kBJavaScriptView Raw
1// Licensed to the Software Freedom Conservancy (SFC) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The SFC licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18/**
19 * @fileoverview Defines a handful of utility functions to simplify working
20 * with promises.
21 */
22
23'use strict'
24
25/**
26 * Determines whether a {@code value} should be treated as a promise.
27 * Any object whose "then" property is a function will be considered a promise.
28 *
29 * @param {?} value The value to test.
30 * @return {boolean} Whether the value is a promise.
31 */
32function isPromise(value) {
33 return Object.prototype.toString.call(value) === '[object Promise]'
34}
35
36/**
37 * Creates a promise that will be resolved at a set time in the future.
38 * @param {number} ms The amount of time, in milliseconds, to wait before
39 * resolving the promise.
40 * @return {!Promise<void>} The promise.
41 */
42function delayed(ms) {
43 return new Promise((resolve) => setTimeout(resolve, ms))
44}
45
46/**
47 * Wraps a function that expects a node-style callback as its final
48 * argument. This callback expects two arguments: an error value (which will be
49 * null if the call succeeded), and the success value as the second argument.
50 * The callback will the resolve or reject the returned promise, based on its
51 * arguments.
52 * @param {!Function} fn The function to wrap.
53 * @param {...?} args The arguments to apply to the function, excluding the
54 * final callback.
55 * @return {!Thenable} A promise that will be resolved with the
56 * result of the provided function's callback.
57 */
58function checkedNodeCall(fn, ...args) {
59 return new Promise(function (fulfill, reject) {
60 try {
61 fn(...args, function (error, value) {
62 error ? reject(error) : fulfill(value)
63 })
64 } catch (ex) {
65 reject(ex)
66 }
67 })
68}
69
70/**
71 * Registers a listener to invoke when a promise is resolved, regardless
72 * of whether the promise's value was successfully computed. This function
73 * is synonymous with the {@code finally} clause in a synchronous API:
74 *
75 * // Synchronous API:
76 * try {
77 * doSynchronousWork();
78 * } finally {
79 * cleanUp();
80 * }
81 *
82 * // Asynchronous promise API:
83 * doAsynchronousWork().finally(cleanUp);
84 *
85 * __Note:__ similar to the {@code finally} clause, if the registered
86 * callback returns a rejected promise or throws an error, it will silently
87 * replace the rejection error (if any) from this promise:
88 *
89 * try {
90 * throw Error('one');
91 * } finally {
92 * throw Error('two'); // Hides Error: one
93 * }
94 *
95 * let p = Promise.reject(Error('one'));
96 * promise.finally(p, function() {
97 * throw Error('two'); // Hides Error: one
98 * });
99 *
100 * @param {!IThenable<?>} promise The promise to add the listener to.
101 * @param {function(): (R|IThenable<R>)} callback The function to call when
102 * the promise is resolved.
103 * @return {!Promise<R>} A promise that will be resolved with the callback
104 * result.
105 * @template R
106 */
107async function thenFinally(promise, callback) {
108 try {
109 await Promise.resolve(promise)
110 return callback()
111 } catch (e) {
112 await callback()
113 throw e
114 }
115}
116
117/**
118 * Calls a function for each element in an array and inserts the result into a
119 * new array, which is used as the fulfillment value of the promise returned
120 * by this function.
121 *
122 * If the return value of the mapping function is a promise, this function
123 * will wait for it to be fulfilled before inserting it into the new array.
124 *
125 * If the mapping function throws or returns a rejected promise, the
126 * promise returned by this function will be rejected with the same reason.
127 * Only the first failure will be reported; all subsequent errors will be
128 * silently ignored.
129 *
130 * @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate
131 * over, or a promise that will resolve to said array.
132 * @param {function(this: SELF, TYPE, number, !Array<TYPE>): ?} fn The
133 * function to call for each element in the array. This function should
134 * expect three arguments (the element, the index, and the array itself.
135 * @param {SELF=} self The object to be used as the value of 'this' within `fn`.
136 * @template TYPE, SELF
137 */
138async function map(array, fn, self = undefined) {
139 const v = await Promise.resolve(array)
140 if (!Array.isArray(v)) {
141 throw TypeError('not an array')
142 }
143
144 const arr = /** @type {!Array} */ (v)
145 const n = arr.length
146 const values = new Array(n)
147
148 for (let i = 0; i < n; i++) {
149 if (i in arr) {
150 values[i] = await Promise.resolve(fn.call(self, arr[i], i, arr))
151 }
152 }
153 return values
154}
155
156/**
157 * Calls a function for each element in an array, and if the function returns
158 * true adds the element to a new array.
159 *
160 * If the return value of the filter function is a promise, this function
161 * will wait for it to be fulfilled before determining whether to insert the
162 * element into the new array.
163 *
164 * If the filter function throws or returns a rejected promise, the promise
165 * returned by this function will be rejected with the same reason. Only the
166 * first failure will be reported; all subsequent errors will be silently
167 * ignored.
168 *
169 * @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate
170 * over, or a promise that will resolve to said array.
171 * @param {function(this: SELF, TYPE, number, !Array<TYPE>): (
172 * boolean|IThenable<boolean>)} fn The function
173 * to call for each element in the array.
174 * @param {SELF=} self The object to be used as the value of 'this' within `fn`.
175 * @template TYPE, SELF
176 */
177async function filter(array, fn, self = undefined) {
178 const v = await Promise.resolve(array)
179 if (!Array.isArray(v)) {
180 throw TypeError('not an array')
181 }
182
183 const arr = /** @type {!Array} */ (v)
184 const n = arr.length
185 const values = []
186 let valuesLength = 0
187
188 for (let i = 0; i < n; i++) {
189 if (i in arr) {
190 const value = arr[i]
191 const include = await fn.call(self, value, i, arr)
192 if (include) {
193 values[valuesLength++] = value
194 }
195 }
196 }
197 return values
198}
199
200/**
201 * Returns a promise that will be resolved with the input value in a
202 * fully-resolved state. If the value is an array, each element will be fully
203 * resolved. Likewise, if the value is an object, all keys will be fully
204 * resolved. In both cases, all nested arrays and objects will also be
205 * fully resolved. All fields are resolved in place; the returned promise will
206 * resolve on {@code value} and not a copy.
207 *
208 * Warning: This function makes no checks against objects that contain
209 * cyclical references:
210 *
211 * var value = {};
212 * value['self'] = value;
213 * promise.fullyResolved(value); // Stack overflow.
214 *
215 * @param {*} value The value to fully resolve.
216 * @return {!Thenable} A promise for a fully resolved version
217 * of the input value.
218 */
219async function fullyResolved(value) {
220 value = await Promise.resolve(value)
221 if (Array.isArray(value)) {
222 return fullyResolveKeys(/** @type {!Array} */ (value))
223 }
224
225 if (value && typeof value === 'object') {
226 return fullyResolveKeys(/** @type {!Object} */ (value))
227 }
228
229 if (typeof value === 'function') {
230 return fullyResolveKeys(/** @type {!Object} */ (value))
231 }
232
233 return value
234}
235
236/**
237 * @param {!(Array|Object)} obj the object to resolve.
238 * @return {!Thenable} A promise that will be resolved with the
239 * input object once all of its values have been fully resolved.
240 */
241async function fullyResolveKeys(obj) {
242 const isArray = Array.isArray(obj)
243 const numKeys = isArray ? obj.length : Object.keys(obj).length
244
245 if (!numKeys) {
246 return obj
247 }
248
249 async function forEachProperty(obj, fn) {
250 for (let key in obj) {
251 await fn(obj[key], key)
252 }
253 }
254
255 async function forEachElement(arr, fn) {
256 for (let i = 0; i < arr.length; i++) {
257 await fn(arr[i], i)
258 }
259 }
260
261 const forEachKey = isArray ? forEachElement : forEachProperty
262 await forEachKey(obj, async function (partialValue, key) {
263 if (
264 !Array.isArray(partialValue) &&
265 (!partialValue || typeof partialValue !== 'object')
266 ) {
267 return
268 }
269 let resolvedValue = await fullyResolved(partialValue)
270 obj[key] = resolvedValue
271 })
272 return obj
273}
274
275// PUBLIC API
276
277module.exports = {
278 checkedNodeCall,
279 delayed,
280 filter,
281 finally: thenFinally,
282 fullyResolved,
283 isPromise,
284 map,
285}