Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/selenium-webdriver/lib/promise.js
2884 views
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
const { isObject, isPromise } = require('./util')
26
27
/**
28
* Creates a promise that will be resolved at a set time in the future.
29
* @param {number} ms The amount of time, in milliseconds, to wait before
30
* resolving the promise.
31
* @return {!Promise<void>} The promise.
32
*/
33
function delayed(ms) {
34
return new Promise((resolve) => setTimeout(resolve, ms))
35
}
36
37
/**
38
* Wraps a function that expects a node-style callback as its final
39
* argument. This callback expects two arguments: an error value (which will be
40
* null if the call succeeded), and the success value as the second argument.
41
* The callback will the resolve or reject the returned promise, based on its
42
* arguments.
43
* @param {!Function} fn The function to wrap.
44
* @param {...?} args The arguments to apply to the function, excluding the
45
* final callback.
46
* @return {!Thenable} A promise that will be resolved with the
47
* result of the provided function's callback.
48
*/
49
function checkedNodeCall(fn, ...args) {
50
return new Promise(function (fulfill, reject) {
51
try {
52
fn(...args, function (error, value) {
53
error ? reject(error) : fulfill(value)
54
})
55
} catch (ex) {
56
reject(ex)
57
}
58
})
59
}
60
61
/**
62
* Registers a listener to invoke when a promise is resolved, regardless
63
* of whether the promise's value was successfully computed. This function
64
* is synonymous with the {@code finally} clause in a synchronous API:
65
*
66
* // Synchronous API:
67
* try {
68
* doSynchronousWork();
69
* } finally {
70
* cleanUp();
71
* }
72
*
73
* // Asynchronous promise API:
74
* doAsynchronousWork().finally(cleanUp);
75
*
76
* __Note:__ similar to the {@code finally} clause, if the registered
77
* callback returns a rejected promise or throws an error, it will silently
78
* replace the rejection error (if any) from this promise:
79
*
80
* try {
81
* throw Error('one');
82
* } finally {
83
* throw Error('two'); // Hides Error: one
84
* }
85
*
86
* let p = Promise.reject(Error('one'));
87
* promise.finally(p, function() {
88
* throw Error('two'); // Hides Error: one
89
* });
90
*
91
* @param {!IThenable<?>} promise The promise to add the listener to.
92
* @param {function(): (R|IThenable<R>)} callback The function to call when
93
* the promise is resolved.
94
* @return {!Promise<R>} A promise that will be resolved with the callback
95
* result.
96
* @template R
97
*/
98
async function thenFinally(promise, callback) {
99
try {
100
await Promise.resolve(promise)
101
return callback()
102
} catch (e) {
103
await callback()
104
throw e
105
}
106
}
107
108
/**
109
* Calls a function for each element in an array and inserts the result into a
110
* new array, which is used as the fulfillment value of the promise returned
111
* by this function.
112
*
113
* If the return value of the mapping function is a promise, this function
114
* will wait for it to be fulfilled before inserting it into the new array.
115
*
116
* If the mapping function throws or returns a rejected promise, the
117
* promise returned by this function will be rejected with the same reason.
118
* Only the first failure will be reported; all subsequent errors will be
119
* silently ignored.
120
*
121
* @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate
122
* over, or a promise that will resolve to said array.
123
* @param {function(this: SELF, TYPE, number, !Array<TYPE>): ?} fn The
124
* function to call for each element in the array. This function should
125
* expect three arguments (the element, the index, and the array itself.
126
* @param {SELF=} self The object to be used as the value of 'this' within `fn`.
127
* @template TYPE, SELF
128
*/
129
async function map(array, fn, self = undefined) {
130
const v = await Promise.resolve(array)
131
if (!Array.isArray(v)) {
132
throw TypeError('not an array')
133
}
134
135
const arr = /** @type {!Array} */ (v)
136
const values = []
137
138
for (const [index, item] of arr.entries()) {
139
values.push(await Promise.resolve(fn.call(self, item, index, arr)))
140
}
141
142
return values
143
}
144
145
/**
146
* Calls a function for each element in an array, and if the function returns
147
* true adds the element to a new array.
148
*
149
* If the return value of the filter function is a promise, this function
150
* will wait for it to be fulfilled before determining whether to insert the
151
* element into the new array.
152
*
153
* If the filter function throws or returns a rejected promise, the promise
154
* returned by this function will be rejected with the same reason. Only the
155
* first failure will be reported; all subsequent errors will be silently
156
* ignored.
157
*
158
* @param {!(Array<TYPE>|IThenable<!Array<TYPE>>)} array The array to iterate
159
* over, or a promise that will resolve to said array.
160
* @param {function(this: SELF, TYPE, number, !Array<TYPE>): (
161
* boolean|IThenable<boolean>)} fn The function
162
* to call for each element in the array.
163
* @param {SELF=} self The object to be used as the value of 'this' within `fn`.
164
* @template TYPE, SELF
165
*/
166
async function filter(array, fn, self = undefined) {
167
const v = await Promise.resolve(array)
168
if (!Array.isArray(v)) {
169
throw TypeError('not an array')
170
}
171
172
const arr = /** @type {!Array} */ (v)
173
const values = []
174
175
for (const [index, item] of arr.entries()) {
176
const isConditionTrue = await Promise.resolve(fn.call(self, item, index, arr))
177
if (isConditionTrue) {
178
values.push(item)
179
}
180
}
181
182
return values
183
}
184
185
/**
186
* Returns a promise that will be resolved with the input value in a
187
* fully-resolved state. If the value is an array, each element will be fully
188
* resolved. Likewise, if the value is an object, all keys will be fully
189
* resolved. In both cases, all nested arrays and objects will also be
190
* fully resolved. All fields are resolved in place; the returned promise will
191
* resolve on {@code value} and not a copy.
192
*
193
* Warning: This function makes no checks against objects that contain
194
* cyclical references:
195
*
196
* var value = {};
197
* value['self'] = value;
198
* promise.fullyResolved(value); // Stack overflow.
199
*
200
* @param {*} value The value to fully resolve.
201
* @return {!Thenable} A promise for a fully resolved version
202
* of the input value.
203
*/
204
async function fullyResolved(value) {
205
value = await Promise.resolve(value)
206
if (Array.isArray(value)) {
207
return fullyResolveKeys(/** @type {!Array} */ (value))
208
}
209
210
if (isObject(value)) {
211
return fullyResolveKeys(/** @type {!Object} */ (value))
212
}
213
214
if (typeof value === 'function') {
215
return fullyResolveKeys(/** @type {!Object} */ (value))
216
}
217
218
return value
219
}
220
221
/**
222
* @param {!(Array|Object)} obj the object to resolve.
223
* @return {!Thenable} A promise that will be resolved with the
224
* input object once all of its values have been fully resolved.
225
*/
226
async function fullyResolveKeys(obj) {
227
const isArray = Array.isArray(obj)
228
const numKeys = isArray ? obj.length : Object.keys(obj).length
229
230
if (!numKeys) {
231
return obj
232
}
233
234
async function forEachProperty(obj, fn) {
235
for (let key in obj) {
236
await fn(obj[key], key)
237
}
238
}
239
240
async function forEachElement(arr, fn) {
241
for (let i = 0; i < arr.length; i++) {
242
await fn(arr[i], i)
243
}
244
}
245
246
const forEachKey = isArray ? forEachElement : forEachProperty
247
await forEachKey(obj, async function (partialValue, key) {
248
if (!Array.isArray(partialValue) && (!partialValue || typeof partialValue !== 'object')) {
249
return
250
}
251
obj[key] = await fullyResolved(partialValue)
252
})
253
return obj
254
}
255
256
// PUBLIC API
257
258
module.exports = {
259
checkedNodeCall,
260
delayed,
261
filter,
262
finally: thenFinally,
263
fullyResolved,
264
isPromise,
265
map,
266
}
267
268