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 |
|
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 | */
|
32 | function 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 | */
|
42 | function 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 | */
|
58 | function 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 | */
|
107 | async 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 | */
|
138 | async 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 | */
|
177 | async 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 | */
|
219 | async 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 | */
|
241 | async 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 |
|
277 | module.exports = {
|
278 | checkedNodeCall,
|
279 | delayed,
|
280 | filter,
|
281 | finally: thenFinally,
|
282 | fullyResolved,
|
283 | isPromise,
|
284 | map,
|
285 | }
|