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
|
20 | *
|
21 | * > ### IMPORTANT NOTICE
|
22 | * >
|
23 | * > The promise manager contained in this module is in the process of being
|
24 | * > phased out in favor of native JavaScript promises. This will be a long
|
25 | * > process and will not be completed until there have been two major LTS Node
|
26 | * > releases (approx. Node v10.0) that support
|
27 | * > [async functions](https://tc39.github.io/ecmascript-asyncawait/).
|
28 | * >
|
29 | * > At this time, the promise manager can be disabled by setting an environment
|
30 | * > variable, `SELENIUM_PROMISE_MANAGER=0`. In the absence of async functions,
|
31 | * > users may use generators with the
|
32 | * > {@link ./promise.consume promise.consume()} function to write "synchronous"
|
33 | * > style tests:
|
34 | * >
|
35 | * > ```js
|
36 | * > const {Builder, By, promise, until} = require('selenium-webdriver');
|
37 | * >
|
38 | * > let result = promise.consume(function* doGoogleSearch() {
|
39 | * > let driver = new Builder().forBrowser('firefox').build();
|
40 | * > yield driver.get('http://www.google.com/ncr');
|
41 | * > yield driver.findElement(By.name('q')).sendKeys('webdriver');
|
42 | * > yield driver.findElement(By.name('btnG')).click();
|
43 | * > yield driver.wait(until.titleIs('webdriver - Google Search'), 1000);
|
44 | * > yield driver.quit();
|
45 | * > });
|
46 | * >
|
47 | * > result.then(_ => console.log('SUCCESS!'),
|
48 | * > e => console.error('FAILURE: ' + e));
|
49 | * > ```
|
50 | * >
|
51 | * > The motiviation behind this change and full deprecation plan are documented
|
52 | * > in [issue 2969](https://github.com/SeleniumHQ/selenium/issues/2969).
|
53 | * >
|
54 | * >
|
55 | *
|
56 | * The promise module is centered around the {@linkplain ControlFlow}, a class
|
57 | * that coordinates the execution of asynchronous tasks. The ControlFlow allows
|
58 | * users to focus on the imperative commands for their script without worrying
|
59 | * about chaining together every single asynchronous action, which can be
|
60 | * tedious and verbose. APIs may be layered on top of the control flow to read
|
61 | * as if they were synchronous. For instance, the core
|
62 | * {@linkplain ./webdriver.WebDriver WebDriver} API is built on top of the
|
63 | * control flow, allowing users to write
|
64 | *
|
65 | * driver.get('http://www.google.com/ncr');
|
66 | * driver.findElement({name: 'q'}).sendKeys('webdriver');
|
67 | * driver.findElement({name: 'btnGn'}).click();
|
68 | *
|
69 | * instead of
|
70 | *
|
71 | * driver.get('http://www.google.com/ncr')
|
72 | * .then(function() {
|
73 | * return driver.findElement({name: 'q'});
|
74 | * })
|
75 | * .then(function(q) {
|
76 | * return q.sendKeys('webdriver');
|
77 | * })
|
78 | * .then(function() {
|
79 | * return driver.findElement({name: 'btnG'});
|
80 | * })
|
81 | * .then(function(btnG) {
|
82 | * return btnG.click();
|
83 | * });
|
84 | *
|
85 | * ## Tasks and Task Queues
|
86 | *
|
87 | * The control flow is based on the concept of tasks and task queues. Tasks are
|
88 | * functions that define the basic unit of work for the control flow to execute.
|
89 | * Each task is scheduled via {@link ControlFlow#execute()}, which will return
|
90 | * a {@link ManagedPromise ManagedPromise} that will be resolved with the task's
|
91 | * result.
|
92 | *
|
93 | * A task queue contains all of the tasks scheduled within a single turn of the
|
94 | * [JavaScript event loop][JSEL]. The control flow will create a new task queue
|
95 | * the first time a task is scheduled within an event loop.
|
96 | *
|
97 | * var flow = promise.controlFlow();
|
98 | * flow.execute(foo); // Creates a new task queue and inserts foo.
|
99 | * flow.execute(bar); // Inserts bar into the same queue as foo.
|
100 | * setTimeout(function() {
|
101 | * flow.execute(baz); // Creates a new task queue and inserts baz.
|
102 | * }, 0);
|
103 | *
|
104 | * Whenever the control flow creates a new task queue, it will automatically
|
105 | * begin executing tasks in the next available turn of the event loop. This
|
106 | * execution is scheduled using a "micro-task" timer, such as a (native)
|
107 | * `ManagedPromise.then()` callback.
|
108 | *
|
109 | * setTimeout(() => console.log('a'));
|
110 | * ManagedPromise.resolve().then(() => console.log('b')); // A native promise.
|
111 | * flow.execute(() => console.log('c'));
|
112 | * ManagedPromise.resolve().then(() => console.log('d'));
|
113 | * setTimeout(() => console.log('fin'));
|
114 | * // b
|
115 | * // c
|
116 | * // d
|
117 | * // a
|
118 | * // fin
|
119 | *
|
120 | * In the example above, b/c/d is logged before a/fin because native promises
|
121 | * and this module use "micro-task" timers, which have a higher priority than
|
122 | * "macro-tasks" like `setTimeout`.
|
123 | *
|
124 | * ## Task Execution
|
125 | *
|
126 | * Upon creating a task queue, and whenever an exisiting queue completes a task,
|
127 | * the control flow will schedule a micro-task timer to process any scheduled
|
128 | * tasks. This ensures no task is ever started within the same turn of the
|
129 | * JavaScript event loop in which it was scheduled, nor is a task ever started
|
130 | * within the same turn that another finishes.
|
131 | *
|
132 | * When the execution timer fires, a single task will be dequeued and executed.
|
133 | * There are several important events that may occur while executing a task
|
134 | * function:
|
135 | *
|
136 | * 1. A new task queue is created by a call to {@link ControlFlow#execute()}.
|
137 | * Any tasks scheduled within this task queue are considered subtasks of the
|
138 | * current task.
|
139 | * 2. The task function throws an error. Any scheduled tasks are immediately
|
140 | * discarded and the task's promised result (previously returned by
|
141 | * {@link ControlFlow#execute()}) is immediately rejected with the thrown
|
142 | * error.
|
143 | * 3. The task function returns sucessfully.
|
144 | *
|
145 | * If a task function created a new task queue, the control flow will wait for
|
146 | * that queue to complete before processing the task result. If the queue
|
147 | * completes without error, the flow will settle the task's promise with the
|
148 | * value originaly returned by the task function. On the other hand, if the task
|
149 | * queue termintes with an error, the task's promise will be rejected with that
|
150 | * error.
|
151 | *
|
152 | * flow.execute(function() {
|
153 | * flow.execute(() => console.log('a'));
|
154 | * flow.execute(() => console.log('b'));
|
155 | * });
|
156 | * flow.execute(() => console.log('c'));
|
157 | * // a
|
158 | * // b
|
159 | * // c
|
160 | *
|
161 | * ## ManagedPromise Integration
|
162 | *
|
163 | * In addition to the {@link ControlFlow} class, the promise module also exports
|
164 | * a [ManagedPromise/A+] {@linkplain ManagedPromise implementation} that is deeply
|
165 | * integrated with the ControlFlow. First and foremost, each promise
|
166 | * {@linkplain ManagedPromise#then() callback} is scheduled with the
|
167 | * control flow as a task. As a result, each callback is invoked in its own turn
|
168 | * of the JavaScript event loop with its own task queue. If any tasks are
|
169 | * scheduled within a callback, the callback's promised result will not be
|
170 | * settled until the task queue has completed.
|
171 | *
|
172 | * promise.fulfilled().then(function() {
|
173 | * flow.execute(function() {
|
174 | * console.log('b');
|
175 | * });
|
176 | * }).then(() => console.log('a'));
|
177 | * // b
|
178 | * // a
|
179 | *
|
180 | * ### Scheduling ManagedPromise Callbacks <a id="scheduling_callbacks"></a>
|
181 | *
|
182 | * How callbacks are scheduled in the control flow depends on when they are
|
183 | * attached to the promise. Callbacks attached to a _previously_ resolved
|
184 | * promise are immediately enqueued as subtasks of the currently running task.
|
185 | *
|
186 | * var p = promise.fulfilled();
|
187 | * flow.execute(function() {
|
188 | * flow.execute(() => console.log('A'));
|
189 | * p.then( () => console.log('B'));
|
190 | * flow.execute(() => console.log('C'));
|
191 | * p.then( () => console.log('D'));
|
192 | * }).then(function() {
|
193 | * console.log('fin');
|
194 | * });
|
195 | * // A
|
196 | * // B
|
197 | * // C
|
198 | * // D
|
199 | * // fin
|
200 | *
|
201 | * When a promise is resolved while a task function is on the call stack, any
|
202 | * callbacks also registered in that stack frame are scheduled as if the promise
|
203 | * were already resolved:
|
204 | *
|
205 | * var d = promise.defer();
|
206 | * flow.execute(function() {
|
207 | * flow.execute( () => console.log('A'));
|
208 | * d.promise.then(() => console.log('B'));
|
209 | * flow.execute( () => console.log('C'));
|
210 | * d.promise.then(() => console.log('D'));
|
211 | *
|
212 | * d.fulfill();
|
213 | * }).then(function() {
|
214 | * console.log('fin');
|
215 | * });
|
216 | * // A
|
217 | * // B
|
218 | * // C
|
219 | * // D
|
220 | * // fin
|
221 | *
|
222 | * Callbacks attached to an _unresolved_ promise within a task function are
|
223 | * only weakly scheduled as subtasks and will be dropped if they reach the
|
224 | * front of the queue before the promise is resolved. In the example below, the
|
225 | * callbacks for `B` & `D` are dropped as sub-tasks since they are attached to
|
226 | * an unresolved promise when they reach the front of the task queue.
|
227 | *
|
228 | * var d = promise.defer();
|
229 | * flow.execute(function() {
|
230 | * flow.execute( () => console.log('A'));
|
231 | * d.promise.then(() => console.log('B'));
|
232 | * flow.execute( () => console.log('C'));
|
233 | * d.promise.then(() => console.log('D'));
|
234 | *
|
235 | * setTimeout(d.fulfill, 20);
|
236 | * }).then(function() {
|
237 | * console.log('fin')
|
238 | * });
|
239 | * // A
|
240 | * // C
|
241 | * // fin
|
242 | * // B
|
243 | * // D
|
244 | *
|
245 | * If a promise is resolved while a task function is on the call stack, any
|
246 | * previously registered and unqueued callbacks (i.e. either attached while no
|
247 | * task was on the call stack, or previously dropped as described above) act as
|
248 | * _interrupts_ and are inserted at the front of the task queue. If multiple
|
249 | * promises are fulfilled, their interrupts are enqueued in the order the
|
250 | * promises are resolved.
|
251 | *
|
252 | * var d1 = promise.defer();
|
253 | * d1.promise.then(() => console.log('A'));
|
254 | *
|
255 | * var d2 = promise.defer();
|
256 | * d2.promise.then(() => console.log('B'));
|
257 | *
|
258 | * flow.execute(function() {
|
259 | * d1.promise.then(() => console.log('C'));
|
260 | * flow.execute(() => console.log('D'));
|
261 | * });
|
262 | * flow.execute(function() {
|
263 | * flow.execute(() => console.log('E'));
|
264 | * flow.execute(() => console.log('F'));
|
265 | * d1.fulfill();
|
266 | * d2.fulfill();
|
267 | * }).then(function() {
|
268 | * console.log('fin');
|
269 | * });
|
270 | * // D
|
271 | * // A
|
272 | * // C
|
273 | * // B
|
274 | * // E
|
275 | * // F
|
276 | * // fin
|
277 | *
|
278 | * Within a task function (or callback), each step of a promise chain acts as
|
279 | * an interrupt on the task queue:
|
280 | *
|
281 | * var d = promise.defer();
|
282 | * flow.execute(function() {
|
283 | * d.promise.
|
284 | * then(() => console.log('A')).
|
285 | * then(() => console.log('B')).
|
286 | * then(() => console.log('C')).
|
287 | * then(() => console.log('D'));
|
288 | *
|
289 | * flow.execute(() => console.log('E'));
|
290 | * d.fulfill();
|
291 | * }).then(function() {
|
292 | * console.log('fin');
|
293 | * });
|
294 | * // A
|
295 | * // B
|
296 | * // C
|
297 | * // D
|
298 | * // E
|
299 | * // fin
|
300 | *
|
301 | * If there are multiple promise chains derived from a single promise, they are
|
302 | * processed in the order created:
|
303 | *
|
304 | * var d = promise.defer();
|
305 | * flow.execute(function() {
|
306 | * var chain = d.promise.then(() => console.log('A'));
|
307 | *
|
308 | * chain.then(() => console.log('B')).
|
309 | * then(() => console.log('C'));
|
310 | *
|
311 | * chain.then(() => console.log('D')).
|
312 | * then(() => console.log('E'));
|
313 | *
|
314 | * flow.execute(() => console.log('F'));
|
315 | *
|
316 | * d.fulfill();
|
317 | * }).then(function() {
|
318 | * console.log('fin');
|
319 | * });
|
320 | * // A
|
321 | * // B
|
322 | * // C
|
323 | * // D
|
324 | * // E
|
325 | * // F
|
326 | * // fin
|
327 | *
|
328 | * Even though a subtask's promised result will never resolve while the task
|
329 | * function is on the stack, it will be treated as a promise resolved within the
|
330 | * task. In all other scenarios, a task's promise behaves just like a normal
|
331 | * promise. In the sample below, `C/D` is loggged before `B` because the
|
332 | * resolution of `subtask1` interrupts the flow of the enclosing task. Within
|
333 | * the final subtask, `E/F` is logged in order because `subtask1` is a resolved
|
334 | * promise when that task runs.
|
335 | *
|
336 | * flow.execute(function() {
|
337 | * var subtask1 = flow.execute(() => console.log('A'));
|
338 | * var subtask2 = flow.execute(() => console.log('B'));
|
339 | *
|
340 | * subtask1.then(() => console.log('C'));
|
341 | * subtask1.then(() => console.log('D'));
|
342 | *
|
343 | * flow.execute(function() {
|
344 | * flow.execute(() => console.log('E'));
|
345 | * subtask1.then(() => console.log('F'));
|
346 | * });
|
347 | * }).then(function() {
|
348 | * console.log('fin');
|
349 | * });
|
350 | * // A
|
351 | * // C
|
352 | * // D
|
353 | * // B
|
354 | * // E
|
355 | * // F
|
356 | * // fin
|
357 | *
|
358 | * Finally, consider the following:
|
359 | *
|
360 | * var d = promise.defer();
|
361 | * d.promise.then(() => console.log('A'));
|
362 | * d.promise.then(() => console.log('B'));
|
363 | *
|
364 | * flow.execute(function() {
|
365 | * flow.execute( () => console.log('C'));
|
366 | * d.promise.then(() => console.log('D'));
|
367 | *
|
368 | * flow.execute( () => console.log('E'));
|
369 | * d.promise.then(() => console.log('F'));
|
370 | *
|
371 | * d.fulfill();
|
372 | *
|
373 | * flow.execute( () => console.log('G'));
|
374 | * d.promise.then(() => console.log('H'));
|
375 | * }).then(function() {
|
376 | * console.log('fin');
|
377 | * });
|
378 | * // A
|
379 | * // B
|
380 | * // C
|
381 | * // D
|
382 | * // E
|
383 | * // F
|
384 | * // G
|
385 | * // H
|
386 | * // fin
|
387 | *
|
388 | * In this example, callbacks are registered on `d.promise` both before and
|
389 | * during the invocation of the task function. When `d.fulfill()` is called,
|
390 | * the callbacks registered before the task (`A` & `B`) are registered as
|
391 | * interrupts. The remaining callbacks were all attached within the task and
|
392 | * are scheduled in the flow as standard tasks.
|
393 | *
|
394 | * ## Generator Support
|
395 | *
|
396 | * [Generators][GF] may be scheduled as tasks within a control flow or attached
|
397 | * as callbacks to a promise. Each time the generator yields a promise, the
|
398 | * control flow will wait for that promise to settle before executing the next
|
399 | * iteration of the generator. The yielded promise's fulfilled value will be
|
400 | * passed back into the generator:
|
401 | *
|
402 | * flow.execute(function* () {
|
403 | * var d = promise.defer();
|
404 | *
|
405 | * setTimeout(() => console.log('...waiting...'), 25);
|
406 | * setTimeout(() => d.fulfill(123), 50);
|
407 | *
|
408 | * console.log('start: ' + Date.now());
|
409 | *
|
410 | * var value = yield d.promise;
|
411 | * console.log('mid: %d; value = %d', Date.now(), value);
|
412 | *
|
413 | * yield promise.delayed(10);
|
414 | * console.log('end: ' + Date.now());
|
415 | * }).then(function() {
|
416 | * console.log('fin');
|
417 | * });
|
418 | * // start: 0
|
419 | * // ...waiting...
|
420 | * // mid: 50; value = 123
|
421 | * // end: 60
|
422 | * // fin
|
423 | *
|
424 | * Yielding the result of a promise chain will wait for the entire chain to
|
425 | * complete:
|
426 | *
|
427 | * promise.fulfilled().then(function* () {
|
428 | * console.log('start: ' + Date.now());
|
429 | *
|
430 | * var value = yield flow.
|
431 | * execute(() => console.log('A')).
|
432 | * then( () => console.log('B')).
|
433 | * then( () => 123);
|
434 | *
|
435 | * console.log('mid: %s; value = %d', Date.now(), value);
|
436 | *
|
437 | * yield flow.execute(() => console.log('C'));
|
438 | * }).then(function() {
|
439 | * console.log('fin');
|
440 | * });
|
441 | * // start: 0
|
442 | * // A
|
443 | * // B
|
444 | * // mid: 2; value = 123
|
445 | * // C
|
446 | * // fin
|
447 | *
|
448 | * Yielding a _rejected_ promise will cause the rejected value to be thrown
|
449 | * within the generator function:
|
450 | *
|
451 | * flow.execute(function* () {
|
452 | * console.log('start: ' + Date.now());
|
453 | * try {
|
454 | * yield promise.delayed(10).then(function() {
|
455 | * throw Error('boom');
|
456 | * });
|
457 | * } catch (ex) {
|
458 | * console.log('caught time: ' + Date.now());
|
459 | * console.log(ex.message);
|
460 | * }
|
461 | * });
|
462 | * // start: 0
|
463 | * // caught time: 10
|
464 | * // boom
|
465 | *
|
466 | * # Error Handling
|
467 | *
|
468 | * ES6 promises do not require users to handle a promise rejections. This can
|
469 | * result in subtle bugs as the rejections are silently "swallowed" by the
|
470 | * ManagedPromise class.
|
471 | *
|
472 | * ManagedPromise.reject(Error('boom'));
|
473 | * // ... *crickets* ...
|
474 | *
|
475 | * Selenium's promise module, on the other hand, requires that every rejection
|
476 | * be explicitly handled. When a {@linkplain ManagedPromise ManagedPromise} is
|
477 | * rejected and no callbacks are defined on that promise, it is considered an
|
478 | * _unhandled rejection_ and reproted to the active task queue. If the rejection
|
479 | * remains unhandled after a single turn of the [event loop][JSEL] (scheduled
|
480 | * with a micro-task), it will propagate up the stack.
|
481 | *
|
482 | * ## Error Propagation
|
483 | *
|
484 | * If an unhandled rejection occurs within a task function, that task's promised
|
485 | * result is rejected and all remaining subtasks are discarded:
|
486 | *
|
487 | * flow.execute(function() {
|
488 | * // No callbacks registered on promise -> unhandled rejection
|
489 | * promise.rejected(Error('boom'));
|
490 | * flow.execute(function() { console.log('this will never run'); });
|
491 | * }).catch(function(e) {
|
492 | * console.log(e.message);
|
493 | * });
|
494 | * // boom
|
495 | *
|
496 | * The promised results for discarded tasks are silently rejected with a
|
497 | * cancellation error and existing callback chains will never fire.
|
498 | *
|
499 | * flow.execute(function() {
|
500 | * promise.rejected(Error('boom'));
|
501 | * flow.execute(function() { console.log('a'); }).
|
502 | * then(function() { console.log('b'); });
|
503 | * }).catch(function(e) {
|
504 | * console.log(e.message);
|
505 | * });
|
506 | * // boom
|
507 | *
|
508 | * An unhandled rejection takes precedence over a task function's returned
|
509 | * result, even if that value is another promise:
|
510 | *
|
511 | * flow.execute(function() {
|
512 | * promise.rejected(Error('boom'));
|
513 | * return flow.execute(someOtherTask);
|
514 | * }).catch(function(e) {
|
515 | * console.log(e.message);
|
516 | * });
|
517 | * // boom
|
518 | *
|
519 | * If there are multiple unhandled rejections within a task, they are packaged
|
520 | * in a {@link MultipleUnhandledRejectionError}, which has an `errors` property
|
521 | * that is a `Set` of the recorded unhandled rejections:
|
522 | *
|
523 | * flow.execute(function() {
|
524 | * promise.rejected(Error('boom1'));
|
525 | * promise.rejected(Error('boom2'));
|
526 | * }).catch(function(ex) {
|
527 | * console.log(ex instanceof MultipleUnhandledRejectionError);
|
528 | * for (var e of ex.errors) {
|
529 | * console.log(e.message);
|
530 | * }
|
531 | * });
|
532 | * // boom1
|
533 | * // boom2
|
534 | *
|
535 | * When a subtask is discarded due to an unreported rejection in its parent
|
536 | * frame, the existing callbacks on that task will never settle and the
|
537 | * callbacks will not be invoked. If a new callback is attached ot the subtask
|
538 | * _after_ it has been discarded, it is handled the same as adding a callback
|
539 | * to a cancelled promise: the error-callback path is invoked. This behavior is
|
540 | * intended to handle cases where the user saves a reference to a task promise,
|
541 | * as illustrated below.
|
542 | *
|
543 | * var subTask;
|
544 | * flow.execute(function() {
|
545 | * promise.rejected(Error('boom'));
|
546 | * subTask = flow.execute(function() {});
|
547 | * }).catch(function(e) {
|
548 | * console.log(e.message);
|
549 | * }).then(function() {
|
550 | * return subTask.then(
|
551 | * () => console.log('subtask success!'),
|
552 | * (e) => console.log('subtask failed:\n' + e));
|
553 | * });
|
554 | * // boom
|
555 | * // subtask failed:
|
556 | * // DiscardedTaskError: Task was discarded due to a previous failure: boom
|
557 | *
|
558 | * When a subtask fails, its promised result is treated the same as any other
|
559 | * promise: it must be handled within one turn of the rejection or the unhandled
|
560 | * rejection is propagated to the parent task. This means users can catch errors
|
561 | * from complex flows from the top level task:
|
562 | *
|
563 | * flow.execute(function() {
|
564 | * flow.execute(function() {
|
565 | * flow.execute(function() {
|
566 | * throw Error('fail!');
|
567 | * });
|
568 | * });
|
569 | * }).catch(function(e) {
|
570 | * console.log(e.message);
|
571 | * });
|
572 | * // fail!
|
573 | *
|
574 | * ## Unhandled Rejection Events
|
575 | *
|
576 | * When an unhandled rejection propagates to the root of the control flow, the
|
577 | * flow will emit an __uncaughtException__ event. If no listeners are registered
|
578 | * on the flow, the error will be rethrown to the global error handler: an
|
579 | * __uncaughtException__ event from the
|
580 | * [`process`](https://nodejs.org/api/process.html) object in node, or
|
581 | * `window.onerror` when running in a browser.
|
582 | *
|
583 | * Bottom line: you __*must*__ handle rejected promises.
|
584 | *
|
585 | * # ManagedPromise/A+ Compatibility
|
586 | *
|
587 | * This `promise` module is compliant with the [ManagedPromise/A+][] specification
|
588 | * except for sections `2.2.6.1` and `2.2.6.2`:
|
589 | *
|
590 | * >
|
591 | * > - `then` may be called multiple times on the same promise.
|
592 | * > - If/when `promise` is fulfilled, all respective `onFulfilled` callbacks
|
593 | * > must execute in the order of their originating calls to `then`.
|
594 | * > - If/when `promise` is rejected, all respective `onRejected` callbacks
|
595 | * > must execute in the order of their originating calls to `then`.
|
596 | * >
|
597 | *
|
598 | * Specifically, the conformance tests contains the following scenario (for
|
599 | * brevity, only the fulfillment version is shown):
|
600 | *
|
601 | * var p1 = ManagedPromise.resolve();
|
602 | * p1.then(function() {
|
603 | * console.log('A');
|
604 | * p1.then(() => console.log('B'));
|
605 | * });
|
606 | * p1.then(() => console.log('C'));
|
607 | * // A
|
608 | * // C
|
609 | * // B
|
610 | *
|
611 | * Since the [ControlFlow](#scheduling_callbacks) executes promise callbacks as
|
612 | * tasks, with this module, the result would be
|
613 | *
|
614 | * var p2 = promise.fulfilled();
|
615 | * p2.then(function() {
|
616 | * console.log('A');
|
617 | * p2.then(() => console.log('B');
|
618 | * });
|
619 | * p2.then(() => console.log('C'));
|
620 | * // A
|
621 | * // B
|
622 | * // C
|
623 | *
|
624 | * [JSEL]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
|
625 | * [GF]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
|
626 | * [ManagedPromise/A+]: https://promisesaplus.com/
|
627 | */
|
628 |
|
629 | ;
|
630 |
|
631 | const events = require('./events');
|
632 | const logging = require('./logging');
|
633 |
|
634 |
|
635 | /**
|
636 | * Alias to help with readability and differentiate types.
|
637 | * @const
|
638 | */
|
639 | const NativePromise = Promise;
|
640 |
|
641 |
|
642 | /**
|
643 | * Whether to append traces of `then` to rejection errors.
|
644 | * @type {boolean}
|
645 | */
|
646 | var LONG_STACK_TRACES = false; // TODO: this should not be CONSTANT_CASE
|
647 |
|
648 |
|
649 | /** @const */
|
650 | const LOG = logging.getLogger('promise');
|
651 |
|
652 |
|
653 | const UNIQUE_IDS = new WeakMap;
|
654 | let nextId = 1;
|
655 |
|
656 |
|
657 | function getUid(obj) {
|
658 | let id = UNIQUE_IDS.get(obj);
|
659 | if (!id) {
|
660 | id = nextId;
|
661 | nextId += 1;
|
662 | UNIQUE_IDS.set(obj, id);
|
663 | }
|
664 | return id;
|
665 | }
|
666 |
|
667 |
|
668 | /**
|
669 | * Runs the given function after a micro-task yield.
|
670 | * @param {function()} fn The function to run.
|
671 | */
|
672 | function asyncRun(fn) {
|
673 | NativePromise.resolve().then(function() {
|
674 | try {
|
675 | fn();
|
676 | } catch (ignored) {
|
677 | // Do nothing.
|
678 | }
|
679 | });
|
680 | }
|
681 |
|
682 | /**
|
683 | * @param {number} level What level of verbosity to log with.
|
684 | * @param {(string|function(this: T): string)} loggable The message to log.
|
685 | * @param {T=} opt_self The object in whose context to run the loggable
|
686 | * function.
|
687 | * @template T
|
688 | */
|
689 | function vlog(level, loggable, opt_self) {
|
690 | var logLevel = logging.Level.FINE;
|
691 | if (level > 1) {
|
692 | logLevel = logging.Level.FINEST;
|
693 | } else if (level > 0) {
|
694 | logLevel = logging.Level.FINER;
|
695 | }
|
696 |
|
697 | if (typeof loggable === 'function') {
|
698 | loggable = loggable.bind(opt_self);
|
699 | }
|
700 |
|
701 | LOG.log(logLevel, loggable);
|
702 | }
|
703 |
|
704 |
|
705 | /**
|
706 | * Generates an error to capture the current stack trace.
|
707 | * @param {string} name Error name for this stack trace.
|
708 | * @param {string} msg Message to record.
|
709 | * @param {Function=} opt_topFn The function that should appear at the top of
|
710 | * the stack; only applicable in V8.
|
711 | * @return {!Error} The generated error.
|
712 | */
|
713 | function captureStackTrace(name, msg, opt_topFn) {
|
714 | var e = Error(msg);
|
715 | e.name = name;
|
716 | if (Error.captureStackTrace) {
|
717 | Error.captureStackTrace(e, opt_topFn);
|
718 | } else {
|
719 | var stack = Error().stack;
|
720 | if (stack) {
|
721 | e.stack = e.toString();
|
722 | e.stack += '\n' + stack;
|
723 | }
|
724 | }
|
725 | return e;
|
726 | }
|
727 |
|
728 |
|
729 | /**
|
730 | * Error used when the computation of a promise is cancelled.
|
731 | */
|
732 | class CancellationError extends Error {
|
733 | /**
|
734 | * @param {string=} opt_msg The cancellation message.
|
735 | */
|
736 | constructor(opt_msg) {
|
737 | super(opt_msg);
|
738 |
|
739 | /** @override */
|
740 | this.name = this.constructor.name;
|
741 |
|
742 | /** @private {boolean} */
|
743 | this.silent_ = false;
|
744 | }
|
745 |
|
746 | /**
|
747 | * Wraps the given error in a CancellationError.
|
748 | *
|
749 | * @param {*} error The error to wrap.
|
750 | * @param {string=} opt_msg The prefix message to use.
|
751 | * @return {!CancellationError} A cancellation error.
|
752 | */
|
753 | static wrap(error, opt_msg) {
|
754 | var message;
|
755 | if (error instanceof CancellationError) {
|
756 | return new CancellationError(
|
757 | opt_msg ? (opt_msg + ': ' + error.message) : error.message);
|
758 | } else if (opt_msg) {
|
759 | message = opt_msg;
|
760 | if (error) {
|
761 | message += ': ' + error;
|
762 | }
|
763 | return new CancellationError(message);
|
764 | }
|
765 | if (error) {
|
766 | message = error + '';
|
767 | }
|
768 | return new CancellationError(message);
|
769 | }
|
770 | }
|
771 |
|
772 |
|
773 | /**
|
774 | * Error used to cancel tasks when a control flow is reset.
|
775 | * @final
|
776 | */
|
777 | class FlowResetError extends CancellationError {
|
778 | constructor() {
|
779 | super('ControlFlow was reset');
|
780 | this.silent_ = true;
|
781 | }
|
782 | }
|
783 |
|
784 |
|
785 | /**
|
786 | * Error used to cancel tasks that have been discarded due to an uncaught error
|
787 | * reported earlier in the control flow.
|
788 | * @final
|
789 | */
|
790 | class DiscardedTaskError extends CancellationError {
|
791 | /** @param {*} error The original error. */
|
792 | constructor(error) {
|
793 | if (error instanceof DiscardedTaskError) {
|
794 | return /** @type {!DiscardedTaskError} */(error);
|
795 | }
|
796 |
|
797 | var msg = '';
|
798 | if (error) {
|
799 | msg = ': ' + (
|
800 | typeof error.message === 'string' ? error.message : error);
|
801 | }
|
802 |
|
803 | super('Task was discarded due to a previous failure' + msg);
|
804 | this.silent_ = true;
|
805 | }
|
806 | }
|
807 |
|
808 |
|
809 | /**
|
810 | * Error used when there are multiple unhandled promise rejections detected
|
811 | * within a task or callback.
|
812 | *
|
813 | * @final
|
814 | */
|
815 | class MultipleUnhandledRejectionError extends Error {
|
816 | /**
|
817 | * @param {!(Set<*>)} errors The errors to report.
|
818 | */
|
819 | constructor(errors) {
|
820 | super('Multiple unhandled promise rejections reported');
|
821 |
|
822 | /** @override */
|
823 | this.name = this.constructor.name;
|
824 |
|
825 | /** @type {!Set<*>} */
|
826 | this.errors = errors;
|
827 | }
|
828 | }
|
829 |
|
830 |
|
831 | /**
|
832 | * Property used to flag constructor's as implementing the Thenable interface
|
833 | * for runtime type checking.
|
834 | * @const
|
835 | */
|
836 | const IMPLEMENTED_BY_SYMBOL = Symbol('promise.Thenable');
|
837 | const CANCELLABLE_SYMBOL = Symbol('promise.CancellableThenable');
|
838 |
|
839 |
|
840 | /**
|
841 | * @param {function(new: ?)} ctor
|
842 | * @param {!Object} symbol
|
843 | */
|
844 | function addMarkerSymbol(ctor, symbol) {
|
845 | try {
|
846 | ctor.prototype[symbol] = true;
|
847 | } catch (ignored) {
|
848 | // Property access denied?
|
849 | }
|
850 | }
|
851 |
|
852 |
|
853 | /**
|
854 | * @param {*} object
|
855 | * @param {!Object} symbol
|
856 | * @return {boolean}
|
857 | */
|
858 | function hasMarkerSymbol(object, symbol) {
|
859 | if (!object) {
|
860 | return false;
|
861 | }
|
862 | try {
|
863 | return !!object[symbol];
|
864 | } catch (e) {
|
865 | return false; // Property access seems to be forbidden.
|
866 | }
|
867 | }
|
868 |
|
869 |
|
870 | /**
|
871 | * Thenable is a promise-like object with a {@code then} method which may be
|
872 | * used to schedule callbacks on a promised value.
|
873 | *
|
874 | * @record
|
875 | * @extends {IThenable<T>}
|
876 | * @template T
|
877 | */
|
878 | class Thenable {
|
879 | /**
|
880 | * Adds a property to a class prototype to allow runtime checks of whether
|
881 | * instances of that class implement the Thenable interface.
|
882 | * @param {function(new: Thenable, ...?)} ctor The
|
883 | * constructor whose prototype to modify.
|
884 | */
|
885 | static addImplementation(ctor) {
|
886 | addMarkerSymbol(ctor, IMPLEMENTED_BY_SYMBOL);
|
887 | }
|
888 |
|
889 | /**
|
890 | * Checks if an object has been tagged for implementing the Thenable
|
891 | * interface as defined by {@link Thenable.addImplementation}.
|
892 | * @param {*} object The object to test.
|
893 | * @return {boolean} Whether the object is an implementation of the Thenable
|
894 | * interface.
|
895 | */
|
896 | static isImplementation(object) {
|
897 | return hasMarkerSymbol(object, IMPLEMENTED_BY_SYMBOL);
|
898 | }
|
899 |
|
900 | /**
|
901 | * Registers listeners for when this instance is resolved.
|
902 | *
|
903 | * @param {?(function(T): (R|IThenable<R>))=} opt_callback The
|
904 | * function to call if this promise is successfully resolved. The function
|
905 | * should expect a single argument: the promise's resolved value.
|
906 | * @param {?(function(*): (R|IThenable<R>))=} opt_errback
|
907 | * The function to call if this promise is rejected. The function should
|
908 | * expect a single argument: the rejection reason.
|
909 | * @return {!Thenable<R>} A new promise which will be resolved with the result
|
910 | * of the invoked callback.
|
911 | * @template R
|
912 | */
|
913 | then(opt_callback, opt_errback) {}
|
914 |
|
915 | /**
|
916 | * Registers a listener for when this promise is rejected. This is synonymous
|
917 | * with the {@code catch} clause in a synchronous API:
|
918 | *
|
919 | * // Synchronous API:
|
920 | * try {
|
921 | * doSynchronousWork();
|
922 | * } catch (ex) {
|
923 | * console.error(ex);
|
924 | * }
|
925 | *
|
926 | * // Asynchronous promise API:
|
927 | * doAsynchronousWork().catch(function(ex) {
|
928 | * console.error(ex);
|
929 | * });
|
930 | *
|
931 | * @param {function(*): (R|IThenable<R>)} errback The
|
932 | * function to call if this promise is rejected. The function should
|
933 | * expect a single argument: the rejection reason.
|
934 | * @return {!Thenable<R>} A new promise which will be resolved with the result
|
935 | * of the invoked callback.
|
936 | * @template R
|
937 | */
|
938 | catch(errback) {}
|
939 | }
|
940 |
|
941 |
|
942 | /**
|
943 | * Marker interface for objects that allow consumers to request the cancellation
|
944 | * of a promies-based operation. A cancelled promise will be rejected with a
|
945 | * {@link CancellationError}.
|
946 | *
|
947 | * This interface is considered package-private and should not be used outside
|
948 | * of selenium-webdriver.
|
949 | *
|
950 | * @interface
|
951 | * @extends {Thenable<T>}
|
952 | * @template T
|
953 | * @package
|
954 | */
|
955 | class CancellableThenable {
|
956 | /**
|
957 | * @param {function(new: CancellableThenable, ...?)} ctor
|
958 | */
|
959 | static addImplementation(ctor) {
|
960 | Thenable.addImplementation(ctor);
|
961 | addMarkerSymbol(ctor, CANCELLABLE_SYMBOL);
|
962 | }
|
963 |
|
964 | /**
|
965 | * @param {*} object
|
966 | * @return {boolean}
|
967 | */
|
968 | static isImplementation(object) {
|
969 | return hasMarkerSymbol(object, CANCELLABLE_SYMBOL);
|
970 | }
|
971 |
|
972 | /**
|
973 | * Requests the cancellation of the computation of this promise's value,
|
974 | * rejecting the promise in the process. This method is a no-op if the promise
|
975 | * has already been resolved.
|
976 | *
|
977 | * @param {(string|Error)=} opt_reason The reason this promise is being
|
978 | * cancelled. This value will be wrapped in a {@link CancellationError}.
|
979 | */
|
980 | cancel(opt_reason) {}
|
981 | }
|
982 |
|
983 |
|
984 | /**
|
985 | * @enum {string}
|
986 | */
|
987 | const PromiseState = {
|
988 | PENDING: 'pending',
|
989 | BLOCKED: 'blocked',
|
990 | REJECTED: 'rejected',
|
991 | FULFILLED: 'fulfilled'
|
992 | };
|
993 |
|
994 |
|
995 | /**
|
996 | * Internal map used to store cancellation handlers for {@link ManagedPromise}
|
997 | * objects. This is an internal implementation detail used by the
|
998 | * {@link TaskQueue} class to monitor for when a promise is cancelled without
|
999 | * generating an extra promise via then().
|
1000 | *
|
1001 | * @const {!WeakMap<!ManagedPromise, function(!CancellationError)>}
|
1002 | */
|
1003 | const ON_CANCEL_HANDLER = new WeakMap;
|
1004 |
|
1005 |
|
1006 | /**
|
1007 | * Represents the eventual value of a completed operation. Each promise may be
|
1008 | * in one of three states: pending, fulfilled, or rejected. Each promise starts
|
1009 | * in the pending state and may make a single transition to either a
|
1010 | * fulfilled or rejected state, at which point the promise is considered
|
1011 | * resolved.
|
1012 | *
|
1013 | * @implements {CancellableThenable<T>}
|
1014 | * @template T
|
1015 | * @see http://promises-aplus.github.io/promises-spec/
|
1016 | */
|
1017 | class ManagedPromise {
|
1018 | /**
|
1019 | * @param {function(
|
1020 | * function((T|IThenable<T>|Thenable)=),
|
1021 | * function(*=))} resolver
|
1022 | * Function that is invoked immediately to begin computation of this
|
1023 | * promise's value. The function should accept a pair of callback
|
1024 | * functions, one for fulfilling the promise and another for rejecting it.
|
1025 | * @param {ControlFlow=} opt_flow The control flow
|
1026 | * this instance was created under. Defaults to the currently active flow.
|
1027 | */
|
1028 | constructor(resolver, opt_flow) {
|
1029 | if (!usePromiseManager()) {
|
1030 | throw TypeError(
|
1031 | 'Unable to create a managed promise instance: the promise manager has'
|
1032 | + ' been disabled by the SELENIUM_PROMISE_MANAGER environment'
|
1033 | + ' variable: ' + process.env['SELENIUM_PROMISE_MANAGER']);
|
1034 | }
|
1035 | getUid(this);
|
1036 |
|
1037 | /** @private {!ControlFlow} */
|
1038 | this.flow_ = opt_flow || controlFlow();
|
1039 |
|
1040 | /** @private {Error} */
|
1041 | this.stack_ = null;
|
1042 | if (LONG_STACK_TRACES) {
|
1043 | this.stack_ = captureStackTrace('ManagedPromise', 'new', this.constructor);
|
1044 | }
|
1045 |
|
1046 | /** @private {Thenable<?>} */
|
1047 | this.parent_ = null;
|
1048 |
|
1049 | /** @private {Array<!Task>} */
|
1050 | this.callbacks_ = null;
|
1051 |
|
1052 | /** @private {PromiseState} */
|
1053 | this.state_ = PromiseState.PENDING;
|
1054 |
|
1055 | /** @private {boolean} */
|
1056 | this.handled_ = false;
|
1057 |
|
1058 | /** @private {*} */
|
1059 | this.value_ = undefined;
|
1060 |
|
1061 | /** @private {TaskQueue} */
|
1062 | this.queue_ = null;
|
1063 |
|
1064 | try {
|
1065 | var self = this;
|
1066 | resolver(function(value) {
|
1067 | self.resolve_(PromiseState.FULFILLED, value);
|
1068 | }, function(reason) {
|
1069 | self.resolve_(PromiseState.REJECTED, reason);
|
1070 | });
|
1071 | } catch (ex) {
|
1072 | this.resolve_(PromiseState.REJECTED, ex);
|
1073 | }
|
1074 | }
|
1075 |
|
1076 | /** @override */
|
1077 | toString() {
|
1078 | return 'ManagedPromise::' + getUid(this) +
|
1079 | ' {[[PromiseStatus]]: "' + this.state_ + '"}';
|
1080 | }
|
1081 |
|
1082 | /**
|
1083 | * Resolves this promise. If the new value is itself a promise, this function
|
1084 | * will wait for it to be resolved before notifying the registered listeners.
|
1085 | * @param {PromiseState} newState The promise's new state.
|
1086 | * @param {*} newValue The promise's new value.
|
1087 | * @throws {TypeError} If {@code newValue === this}.
|
1088 | * @private
|
1089 | */
|
1090 | resolve_(newState, newValue) {
|
1091 | if (PromiseState.PENDING !== this.state_) {
|
1092 | return;
|
1093 | }
|
1094 |
|
1095 | if (newValue === this) {
|
1096 | // See promise a+, 2.3.1
|
1097 | // http://promises-aplus.github.io/promises-spec/#point-48
|
1098 | newValue = new TypeError('A promise may not resolve to itself');
|
1099 | newState = PromiseState.REJECTED;
|
1100 | }
|
1101 |
|
1102 | this.parent_ = null;
|
1103 | this.state_ = PromiseState.BLOCKED;
|
1104 |
|
1105 | if (newState !== PromiseState.REJECTED) {
|
1106 | if (Thenable.isImplementation(newValue)) {
|
1107 | // 2.3.2
|
1108 | newValue = /** @type {!Thenable} */(newValue);
|
1109 | this.parent_ = newValue;
|
1110 | newValue.then(
|
1111 | this.unblockAndResolve_.bind(this, PromiseState.FULFILLED),
|
1112 | this.unblockAndResolve_.bind(this, PromiseState.REJECTED));
|
1113 | return;
|
1114 |
|
1115 | } else if (newValue
|
1116 | && (typeof newValue === 'object' || typeof newValue === 'function')) {
|
1117 | // 2.3.3
|
1118 |
|
1119 | try {
|
1120 | // 2.3.3.1
|
1121 | var then = newValue['then'];
|
1122 | } catch (e) {
|
1123 | // 2.3.3.2
|
1124 | this.state_ = PromiseState.REJECTED;
|
1125 | this.value_ = e;
|
1126 | this.scheduleNotifications_();
|
1127 | return;
|
1128 | }
|
1129 |
|
1130 | if (typeof then === 'function') {
|
1131 | // 2.3.3.3
|
1132 | this.invokeThen_(/** @type {!Object} */(newValue), then);
|
1133 | return;
|
1134 | }
|
1135 | }
|
1136 | }
|
1137 |
|
1138 | if (newState === PromiseState.REJECTED &&
|
1139 | isError(newValue) && newValue.stack && this.stack_) {
|
1140 | newValue.stack += '\nFrom: ' + (this.stack_.stack || this.stack_);
|
1141 | }
|
1142 |
|
1143 | // 2.3.3.4 and 2.3.4
|
1144 | this.state_ = newState;
|
1145 | this.value_ = newValue;
|
1146 | this.scheduleNotifications_();
|
1147 | }
|
1148 |
|
1149 | /**
|
1150 | * Invokes a thenable's "then" method according to 2.3.3.3 of the promise
|
1151 | * A+ spec.
|
1152 | * @param {!Object} x The thenable object.
|
1153 | * @param {!Function} then The "then" function to invoke.
|
1154 | * @private
|
1155 | */
|
1156 | invokeThen_(x, then) {
|
1157 | var called = false;
|
1158 | var self = this;
|
1159 |
|
1160 | var resolvePromise = function(value) {
|
1161 | if (!called) { // 2.3.3.3.3
|
1162 | called = true;
|
1163 | // 2.3.3.3.1
|
1164 | self.unblockAndResolve_(PromiseState.FULFILLED, value);
|
1165 | }
|
1166 | };
|
1167 |
|
1168 | var rejectPromise = function(reason) {
|
1169 | if (!called) { // 2.3.3.3.3
|
1170 | called = true;
|
1171 | // 2.3.3.3.2
|
1172 | self.unblockAndResolve_(PromiseState.REJECTED, reason);
|
1173 | }
|
1174 | };
|
1175 |
|
1176 | try {
|
1177 | // 2.3.3.3
|
1178 | then.call(x, resolvePromise, rejectPromise);
|
1179 | } catch (e) {
|
1180 | // 2.3.3.3.4.2
|
1181 | rejectPromise(e);
|
1182 | }
|
1183 | }
|
1184 |
|
1185 | /**
|
1186 | * @param {PromiseState} newState The promise's new state.
|
1187 | * @param {*} newValue The promise's new value.
|
1188 | * @private
|
1189 | */
|
1190 | unblockAndResolve_(newState, newValue) {
|
1191 | if (this.state_ === PromiseState.BLOCKED) {
|
1192 | this.state_ = PromiseState.PENDING;
|
1193 | this.resolve_(newState, newValue);
|
1194 | }
|
1195 | }
|
1196 |
|
1197 | /**
|
1198 | * @private
|
1199 | */
|
1200 | scheduleNotifications_() {
|
1201 | vlog(2, () => this + ' scheduling notifications', this);
|
1202 |
|
1203 | ON_CANCEL_HANDLER.delete(this);
|
1204 | if (this.value_ instanceof CancellationError
|
1205 | && this.value_.silent_) {
|
1206 | this.callbacks_ = null;
|
1207 | }
|
1208 |
|
1209 | if (!this.queue_) {
|
1210 | this.queue_ = this.flow_.getActiveQueue_();
|
1211 | }
|
1212 |
|
1213 | if (!this.handled_ &&
|
1214 | this.state_ === PromiseState.REJECTED &&
|
1215 | !(this.value_ instanceof CancellationError)) {
|
1216 | this.queue_.addUnhandledRejection(this);
|
1217 | }
|
1218 | this.queue_.scheduleCallbacks(this);
|
1219 | }
|
1220 |
|
1221 | /** @override */
|
1222 | cancel(opt_reason) {
|
1223 | if (!canCancel(this)) {
|
1224 | return;
|
1225 | }
|
1226 |
|
1227 | if (this.parent_ && canCancel(this.parent_)) {
|
1228 | /** @type {!CancellableThenable} */(this.parent_).cancel(opt_reason);
|
1229 | } else {
|
1230 | var reason = CancellationError.wrap(opt_reason);
|
1231 | let onCancel = ON_CANCEL_HANDLER.get(this);
|
1232 | if (onCancel) {
|
1233 | onCancel(reason);
|
1234 | ON_CANCEL_HANDLER.delete(this);
|
1235 | }
|
1236 |
|
1237 | if (this.state_ === PromiseState.BLOCKED) {
|
1238 | this.unblockAndResolve_(PromiseState.REJECTED, reason);
|
1239 | } else {
|
1240 | this.resolve_(PromiseState.REJECTED, reason);
|
1241 | }
|
1242 | }
|
1243 |
|
1244 | function canCancel(promise) {
|
1245 | if (!(promise instanceof ManagedPromise)) {
|
1246 | return CancellableThenable.isImplementation(promise);
|
1247 | }
|
1248 | return promise.state_ === PromiseState.PENDING
|
1249 | || promise.state_ === PromiseState.BLOCKED;
|
1250 | }
|
1251 | }
|
1252 |
|
1253 | /** @override */
|
1254 | then(opt_callback, opt_errback) {
|
1255 | return this.addCallback_(
|
1256 | opt_callback, opt_errback, 'then', ManagedPromise.prototype.then);
|
1257 | }
|
1258 |
|
1259 | /** @override */
|
1260 | catch(errback) {
|
1261 | return this.addCallback_(
|
1262 | null, errback, 'catch', ManagedPromise.prototype.catch);
|
1263 | }
|
1264 |
|
1265 | /**
|
1266 | * @param {function(): (R|IThenable<R>)} callback
|
1267 | * @return {!ManagedPromise<R>}
|
1268 | * @template R
|
1269 | * @see ./promise.finally()
|
1270 | */
|
1271 | finally(callback) {
|
1272 | let result = thenFinally(this, callback);
|
1273 | return /** @type {!ManagedPromise} */(result);
|
1274 | }
|
1275 |
|
1276 | /**
|
1277 | * Registers a new callback with this promise
|
1278 | * @param {(function(T): (R|IThenable<R>)|null|undefined)} callback The
|
1279 | * fulfillment callback.
|
1280 | * @param {(function(*): (R|IThenable<R>)|null|undefined)} errback The
|
1281 | * rejection callback.
|
1282 | * @param {string} name The callback name.
|
1283 | * @param {!Function} fn The function to use as the top of the stack when
|
1284 | * recording the callback's creation point.
|
1285 | * @return {!ManagedPromise<R>} A new promise which will be resolved with the
|
1286 | * esult of the invoked callback.
|
1287 | * @template R
|
1288 | * @private
|
1289 | */
|
1290 | addCallback_(callback, errback, name, fn) {
|
1291 | if (typeof callback !== 'function' && typeof errback !== 'function') {
|
1292 | return this;
|
1293 | }
|
1294 |
|
1295 | this.handled_ = true;
|
1296 | if (this.queue_) {
|
1297 | this.queue_.clearUnhandledRejection(this);
|
1298 | }
|
1299 |
|
1300 | var cb = new Task(
|
1301 | this.flow_,
|
1302 | this.invokeCallback_.bind(this, callback, errback),
|
1303 | name,
|
1304 | LONG_STACK_TRACES ? {name: 'Promise', top: fn} : undefined);
|
1305 | cb.promise.parent_ = this;
|
1306 |
|
1307 | if (this.state_ !== PromiseState.PENDING &&
|
1308 | this.state_ !== PromiseState.BLOCKED) {
|
1309 | this.flow_.getActiveQueue_().enqueue(cb);
|
1310 | } else {
|
1311 | if (!this.callbacks_) {
|
1312 | this.callbacks_ = [];
|
1313 | }
|
1314 | this.callbacks_.push(cb);
|
1315 | cb.blocked = true;
|
1316 | this.flow_.getActiveQueue_().enqueue(cb);
|
1317 | }
|
1318 |
|
1319 | return cb.promise;
|
1320 | }
|
1321 |
|
1322 | /**
|
1323 | * Invokes a callback function attached to this promise.
|
1324 | * @param {(function(T): (R|IThenable<R>)|null|undefined)} callback The
|
1325 | * fulfillment callback.
|
1326 | * @param {(function(*): (R|IThenable<R>)|null|undefined)} errback The
|
1327 | * rejection callback.
|
1328 | * @template R
|
1329 | * @private
|
1330 | */
|
1331 | invokeCallback_(callback, errback) {
|
1332 | var callbackFn = callback;
|
1333 | if (this.state_ === PromiseState.REJECTED) {
|
1334 | callbackFn = errback;
|
1335 | }
|
1336 |
|
1337 | if (typeof callbackFn === 'function') {
|
1338 | if (isGenerator(callbackFn)) {
|
1339 | return consume(callbackFn, null, this.value_);
|
1340 | }
|
1341 | return callbackFn(this.value_);
|
1342 | } else if (this.state_ === PromiseState.REJECTED) {
|
1343 | throw this.value_;
|
1344 | } else {
|
1345 | return this.value_;
|
1346 | }
|
1347 | }
|
1348 | }
|
1349 | CancellableThenable.addImplementation(ManagedPromise);
|
1350 |
|
1351 |
|
1352 | /**
|
1353 | * @param {!ManagedPromise} promise
|
1354 | * @return {boolean}
|
1355 | */
|
1356 | function isPending(promise) {
|
1357 | return promise.state_ === PromiseState.PENDING;
|
1358 | }
|
1359 |
|
1360 |
|
1361 | /**
|
1362 | * Represents a value that will be resolved at some point in the future. This
|
1363 | * class represents the protected "producer" half of a ManagedPromise - each Deferred
|
1364 | * has a {@code promise} property that may be returned to consumers for
|
1365 | * registering callbacks, reserving the ability to resolve the deferred to the
|
1366 | * producer.
|
1367 | *
|
1368 | * If this Deferred is rejected and there are no listeners registered before
|
1369 | * the next turn of the event loop, the rejection will be passed to the
|
1370 | * {@link ControlFlow} as an unhandled failure.
|
1371 | *
|
1372 | * @template T
|
1373 | */
|
1374 | class Deferred {
|
1375 | /**
|
1376 | * @param {ControlFlow=} opt_flow The control flow this instance was
|
1377 | * created under. This should only be provided during unit tests.
|
1378 | */
|
1379 | constructor(opt_flow) {
|
1380 | var fulfill, reject;
|
1381 |
|
1382 | /** @type {!ManagedPromise<T>} */
|
1383 | this.promise = new ManagedPromise(function(f, r) {
|
1384 | fulfill = f;
|
1385 | reject = r;
|
1386 | }, opt_flow);
|
1387 |
|
1388 | var self = this;
|
1389 | var checkNotSelf = function(value) {
|
1390 | if (value === self) {
|
1391 | throw new TypeError('May not resolve a Deferred with itself');
|
1392 | }
|
1393 | };
|
1394 |
|
1395 | /**
|
1396 | * Resolves this deferred with the given value. It is safe to call this as a
|
1397 | * normal function (with no bound "this").
|
1398 | * @param {(T|IThenable<T>|Thenable)=} opt_value The fulfilled value.
|
1399 | */
|
1400 | this.fulfill = function(opt_value) {
|
1401 | checkNotSelf(opt_value);
|
1402 | fulfill(opt_value);
|
1403 | };
|
1404 |
|
1405 | /**
|
1406 | * Rejects this promise with the given reason. It is safe to call this as a
|
1407 | * normal function (with no bound "this").
|
1408 | * @param {*=} opt_reason The rejection reason.
|
1409 | */
|
1410 | this.reject = function(opt_reason) {
|
1411 | checkNotSelf(opt_reason);
|
1412 | reject(opt_reason);
|
1413 | };
|
1414 | }
|
1415 | }
|
1416 |
|
1417 |
|
1418 | /**
|
1419 | * Tests if a value is an Error-like object. This is more than an straight
|
1420 | * instanceof check since the value may originate from another context.
|
1421 | * @param {*} value The value to test.
|
1422 | * @return {boolean} Whether the value is an error.
|
1423 | */
|
1424 | function isError(value) {
|
1425 | return value instanceof Error ||
|
1426 | (!!value && typeof value === 'object'
|
1427 | && typeof value.message === 'string');
|
1428 | }
|
1429 |
|
1430 |
|
1431 | /**
|
1432 | * Determines whether a {@code value} should be treated as a promise.
|
1433 | * Any object whose "then" property is a function will be considered a promise.
|
1434 | *
|
1435 | * @param {?} value The value to test.
|
1436 | * @return {boolean} Whether the value is a promise.
|
1437 | */
|
1438 | function isPromise(value) {
|
1439 | try {
|
1440 | // Use array notation so the Closure compiler does not obfuscate away our
|
1441 | // contract.
|
1442 | return value
|
1443 | && (typeof value === 'object' || typeof value === 'function')
|
1444 | && typeof value['then'] === 'function';
|
1445 | } catch (ex) {
|
1446 | return false;
|
1447 | }
|
1448 | }
|
1449 |
|
1450 |
|
1451 | /**
|
1452 | * Creates a promise that will be resolved at a set time in the future.
|
1453 | * @param {number} ms The amount of time, in milliseconds, to wait before
|
1454 | * resolving the promise.
|
1455 | * @return {!Thenable} The promise.
|
1456 | */
|
1457 | function delayed(ms) {
|
1458 | return createPromise(resolve => {
|
1459 | setTimeout(() => resolve(), ms);
|
1460 | });
|
1461 | }
|
1462 |
|
1463 |
|
1464 | /**
|
1465 | * Creates a new deferred object.
|
1466 | * @return {!Deferred<T>} The new deferred object.
|
1467 | * @template T
|
1468 | */
|
1469 | function defer() {
|
1470 | return new Deferred();
|
1471 | }
|
1472 |
|
1473 |
|
1474 | /**
|
1475 | * Creates a promise that has been resolved with the given value.
|
1476 | * @param {T=} opt_value The resolved value.
|
1477 | * @return {!ManagedPromise<T>} The resolved promise.
|
1478 | * @template T
|
1479 | */
|
1480 | function fulfilled(opt_value) {
|
1481 | if (opt_value instanceof ManagedPromise) {
|
1482 | return opt_value;
|
1483 | }
|
1484 | return new ManagedPromise(function(fulfill) {
|
1485 | fulfill(opt_value);
|
1486 | });
|
1487 | }
|
1488 |
|
1489 |
|
1490 | /**
|
1491 | * Creates a promise that has been rejected with the given reason.
|
1492 | * @param {*=} opt_reason The rejection reason; may be any value, but is
|
1493 | * usually an Error or a string.
|
1494 | * @return {!ManagedPromise<T>} The rejected promise.
|
1495 | * @template T
|
1496 | */
|
1497 | function rejected(opt_reason) {
|
1498 | if (opt_reason instanceof ManagedPromise) {
|
1499 | return opt_reason;
|
1500 | }
|
1501 | return new ManagedPromise(function(_, reject) {
|
1502 | reject(opt_reason);
|
1503 | });
|
1504 | }
|
1505 |
|
1506 |
|
1507 | /**
|
1508 | * Wraps a function that expects a node-style callback as its final
|
1509 | * argument. This callback expects two arguments: an error value (which will be
|
1510 | * null if the call succeeded), and the success value as the second argument.
|
1511 | * The callback will the resolve or reject the returned promise, based on its
|
1512 | * arguments.
|
1513 | * @param {!Function} fn The function to wrap.
|
1514 | * @param {...?} var_args The arguments to apply to the function, excluding the
|
1515 | * final callback.
|
1516 | * @return {!Thenable} A promise that will be resolved with the
|
1517 | * result of the provided function's callback.
|
1518 | */
|
1519 | function checkedNodeCall(fn, var_args) {
|
1520 | let args = Array.prototype.slice.call(arguments, 1);
|
1521 | return createPromise(function(fulfill, reject) {
|
1522 | try {
|
1523 | args.push(function(error, value) {
|
1524 | error ? reject(error) : fulfill(value);
|
1525 | });
|
1526 | fn.apply(undefined, args);
|
1527 | } catch (ex) {
|
1528 | reject(ex);
|
1529 | }
|
1530 | });
|
1531 | }
|
1532 |
|
1533 | /**
|
1534 | * Registers a listener to invoke when a promise is resolved, regardless
|
1535 | * of whether the promise's value was successfully computed. This function
|
1536 | * is synonymous with the {@code finally} clause in a synchronous API:
|
1537 | *
|
1538 | * // Synchronous API:
|
1539 | * try {
|
1540 | * doSynchronousWork();
|
1541 | * } finally {
|
1542 | * cleanUp();
|
1543 | * }
|
1544 | *
|
1545 | * // Asynchronous promise API:
|
1546 | * doAsynchronousWork().finally(cleanUp);
|
1547 | *
|
1548 | * __Note:__ similar to the {@code finally} clause, if the registered
|
1549 | * callback returns a rejected promise or throws an error, it will silently
|
1550 | * replace the rejection error (if any) from this promise:
|
1551 | *
|
1552 | * try {
|
1553 | * throw Error('one');
|
1554 | * } finally {
|
1555 | * throw Error('two'); // Hides Error: one
|
1556 | * }
|
1557 | *
|
1558 | * let p = Promise.reject(Error('one'));
|
1559 | * promise.finally(p, function() {
|
1560 | * throw Error('two'); // Hides Error: one
|
1561 | * });
|
1562 | *
|
1563 | * @param {!IThenable<?>} promise The promise to add the listener to.
|
1564 | * @param {function(): (R|IThenable<R>)} callback The function to call when
|
1565 | * the promise is resolved.
|
1566 | * @return {!IThenable<R>} A promise that will be resolved with the callback
|
1567 | * result.
|
1568 | * @template R
|
1569 | */
|
1570 | function thenFinally(promise, callback) {
|
1571 | let error;
|
1572 | let mustThrow = false;
|
1573 | return promise.then(function() {
|
1574 | return callback();
|
1575 | }, function(err) {
|
1576 | error = err;
|
1577 | mustThrow = true;
|
1578 | return callback();
|
1579 | }).then(function() {
|
1580 | if (mustThrow) {
|
1581 | throw error;
|
1582 | }
|
1583 | });
|
1584 | }
|
1585 |
|
1586 |
|
1587 | /**
|
1588 | * Registers an observer on a promised {@code value}, returning a new promise
|
1589 | * that will be resolved when the value is. If {@code value} is not a promise,
|
1590 | * then the return promise will be immediately resolved.
|
1591 | * @param {*} value The value to observe.
|
1592 | * @param {Function=} opt_callback The function to call when the value is
|
1593 | * resolved successfully.
|
1594 | * @param {Function=} opt_errback The function to call when the value is
|
1595 | * rejected.
|
1596 | * @return {!Thenable} A new promise.
|
1597 | */
|
1598 | function when(value, opt_callback, opt_errback) {
|
1599 | if (Thenable.isImplementation(value)) {
|
1600 | return value.then(opt_callback, opt_errback);
|
1601 | }
|
1602 |
|
1603 | return createPromise(resolve => resolve(value))
|
1604 | .then(opt_callback, opt_errback);
|
1605 | }
|
1606 |
|
1607 |
|
1608 | /**
|
1609 | * Invokes the appropriate callback function as soon as a promised `value` is
|
1610 | * resolved. This function is similar to `when()`, except it does not return
|
1611 | * a new promise.
|
1612 | * @param {*} value The value to observe.
|
1613 | * @param {Function} callback The function to call when the value is
|
1614 | * resolved successfully.
|
1615 | * @param {Function=} opt_errback The function to call when the value is
|
1616 | * rejected.
|
1617 | */
|
1618 | function asap(value, callback, opt_errback) {
|
1619 | if (isPromise(value)) {
|
1620 | value.then(callback, opt_errback);
|
1621 |
|
1622 | } else if (callback) {
|
1623 | callback(value);
|
1624 | }
|
1625 | }
|
1626 |
|
1627 |
|
1628 | /**
|
1629 | * Given an array of promises, will return a promise that will be fulfilled
|
1630 | * with the fulfillment values of the input array's values. If any of the
|
1631 | * input array's promises are rejected, the returned promise will be rejected
|
1632 | * with the same reason.
|
1633 | *
|
1634 | * @param {!Array<(T|!ManagedPromise<T>)>} arr An array of
|
1635 | * promises to wait on.
|
1636 | * @return {!Thenable<!Array<T>>} A promise that is
|
1637 | * fulfilled with an array containing the fulfilled values of the
|
1638 | * input array, or rejected with the same reason as the first
|
1639 | * rejected value.
|
1640 | * @template T
|
1641 | */
|
1642 | function all(arr) {
|
1643 | return createPromise(function(fulfill, reject) {
|
1644 | var n = arr.length;
|
1645 | var values = [];
|
1646 |
|
1647 | if (!n) {
|
1648 | fulfill(values);
|
1649 | return;
|
1650 | }
|
1651 |
|
1652 | var toFulfill = n;
|
1653 | var onFulfilled = function(index, value) {
|
1654 | values[index] = value;
|
1655 | toFulfill--;
|
1656 | if (toFulfill == 0) {
|
1657 | fulfill(values);
|
1658 | }
|
1659 | };
|
1660 |
|
1661 | function processPromise(index) {
|
1662 | asap(arr[index], function(value) {
|
1663 | onFulfilled(index, value);
|
1664 | }, reject);
|
1665 | }
|
1666 |
|
1667 | for (var i = 0; i < n; ++i) {
|
1668 | processPromise(i);
|
1669 | }
|
1670 | });
|
1671 | }
|
1672 |
|
1673 |
|
1674 | /**
|
1675 | * Calls a function for each element in an array and inserts the result into a
|
1676 | * new array, which is used as the fulfillment value of the promise returned
|
1677 | * by this function.
|
1678 | *
|
1679 | * If the return value of the mapping function is a promise, this function
|
1680 | * will wait for it to be fulfilled before inserting it into the new array.
|
1681 | *
|
1682 | * If the mapping function throws or returns a rejected promise, the
|
1683 | * promise returned by this function will be rejected with the same reason.
|
1684 | * Only the first failure will be reported; all subsequent errors will be
|
1685 | * silently ignored.
|
1686 | *
|
1687 | * @param {!(Array<TYPE>|ManagedPromise<!Array<TYPE>>)} arr The
|
1688 | * array to iterator over, or a promise that will resolve to said array.
|
1689 | * @param {function(this: SELF, TYPE, number, !Array<TYPE>): ?} fn The
|
1690 | * function to call for each element in the array. This function should
|
1691 | * expect three arguments (the element, the index, and the array itself.
|
1692 | * @param {SELF=} opt_self The object to be used as the value of 'this' within
|
1693 | * {@code fn}.
|
1694 | * @template TYPE, SELF
|
1695 | */
|
1696 | function map(arr, fn, opt_self) {
|
1697 | return createPromise(resolve => resolve(arr)).then(v => {
|
1698 | if (!Array.isArray(v)) {
|
1699 | throw TypeError('not an array');
|
1700 | }
|
1701 | var arr = /** @type {!Array} */(v);
|
1702 | return createPromise(function(fulfill, reject) {
|
1703 | var n = arr.length;
|
1704 | var values = new Array(n);
|
1705 | (function processNext(i) {
|
1706 | for (; i < n; i++) {
|
1707 | if (i in arr) {
|
1708 | break;
|
1709 | }
|
1710 | }
|
1711 | if (i >= n) {
|
1712 | fulfill(values);
|
1713 | return;
|
1714 | }
|
1715 | try {
|
1716 | asap(
|
1717 | fn.call(opt_self, arr[i], i, /** @type {!Array} */(arr)),
|
1718 | function(value) {
|
1719 | values[i] = value;
|
1720 | processNext(i + 1);
|
1721 | },
|
1722 | reject);
|
1723 | } catch (ex) {
|
1724 | reject(ex);
|
1725 | }
|
1726 | })(0);
|
1727 | });
|
1728 | });
|
1729 | }
|
1730 |
|
1731 |
|
1732 | /**
|
1733 | * Calls a function for each element in an array, and if the function returns
|
1734 | * true adds the element to a new array.
|
1735 | *
|
1736 | * If the return value of the filter function is a promise, this function
|
1737 | * will wait for it to be fulfilled before determining whether to insert the
|
1738 | * element into the new array.
|
1739 | *
|
1740 | * If the filter function throws or returns a rejected promise, the promise
|
1741 | * returned by this function will be rejected with the same reason. Only the
|
1742 | * first failure will be reported; all subsequent errors will be silently
|
1743 | * ignored.
|
1744 | *
|
1745 | * @param {!(Array<TYPE>|ManagedPromise<!Array<TYPE>>)} arr The
|
1746 | * array to iterator over, or a promise that will resolve to said array.
|
1747 | * @param {function(this: SELF, TYPE, number, !Array<TYPE>): (
|
1748 | * boolean|ManagedPromise<boolean>)} fn The function
|
1749 | * to call for each element in the array.
|
1750 | * @param {SELF=} opt_self The object to be used as the value of 'this' within
|
1751 | * {@code fn}.
|
1752 | * @template TYPE, SELF
|
1753 | */
|
1754 | function filter(arr, fn, opt_self) {
|
1755 | return createPromise(resolve => resolve(arr)).then(v => {
|
1756 | if (!Array.isArray(v)) {
|
1757 | throw TypeError('not an array');
|
1758 | }
|
1759 | var arr = /** @type {!Array} */(v);
|
1760 | return createPromise(function(fulfill, reject) {
|
1761 | var n = arr.length;
|
1762 | var values = [];
|
1763 | var valuesLength = 0;
|
1764 | (function processNext(i) {
|
1765 | for (; i < n; i++) {
|
1766 | if (i in arr) {
|
1767 | break;
|
1768 | }
|
1769 | }
|
1770 | if (i >= n) {
|
1771 | fulfill(values);
|
1772 | return;
|
1773 | }
|
1774 | try {
|
1775 | var value = arr[i];
|
1776 | var include = fn.call(opt_self, value, i, /** @type {!Array} */(arr));
|
1777 | asap(include, function(include) {
|
1778 | if (include) {
|
1779 | values[valuesLength++] = value;
|
1780 | }
|
1781 | processNext(i + 1);
|
1782 | }, reject);
|
1783 | } catch (ex) {
|
1784 | reject(ex);
|
1785 | }
|
1786 | })(0);
|
1787 | });
|
1788 | });
|
1789 | }
|
1790 |
|
1791 |
|
1792 | /**
|
1793 | * Returns a promise that will be resolved with the input value in a
|
1794 | * fully-resolved state. If the value is an array, each element will be fully
|
1795 | * resolved. Likewise, if the value is an object, all keys will be fully
|
1796 | * resolved. In both cases, all nested arrays and objects will also be
|
1797 | * fully resolved. All fields are resolved in place; the returned promise will
|
1798 | * resolve on {@code value} and not a copy.
|
1799 | *
|
1800 | * Warning: This function makes no checks against objects that contain
|
1801 | * cyclical references:
|
1802 | *
|
1803 | * var value = {};
|
1804 | * value['self'] = value;
|
1805 | * promise.fullyResolved(value); // Stack overflow.
|
1806 | *
|
1807 | * @param {*} value The value to fully resolve.
|
1808 | * @return {!Thenable} A promise for a fully resolved version
|
1809 | * of the input value.
|
1810 | */
|
1811 | function fullyResolved(value) {
|
1812 | if (isPromise(value)) {
|
1813 | return when(value, fullyResolveValue);
|
1814 | }
|
1815 | return fullyResolveValue(value);
|
1816 | }
|
1817 |
|
1818 |
|
1819 | /**
|
1820 | * @param {*} value The value to fully resolve. If a promise, assumed to
|
1821 | * already be resolved.
|
1822 | * @return {!Thenable} A promise for a fully resolved version
|
1823 | * of the input value.
|
1824 | */
|
1825 | function fullyResolveValue(value) {
|
1826 | if (Array.isArray(value)) {
|
1827 | return fullyResolveKeys(/** @type {!Array} */ (value));
|
1828 | }
|
1829 |
|
1830 | if (isPromise(value)) {
|
1831 | if (isPromise(value)) {
|
1832 | // We get here when the original input value is a promise that
|
1833 | // resolves to itself. When the user provides us with such a promise,
|
1834 | // trust that it counts as a "fully resolved" value and return it.
|
1835 | // Of course, since it's already a promise, we can just return it
|
1836 | // to the user instead of wrapping it in another promise.
|
1837 | return /** @type {!ManagedPromise} */ (value);
|
1838 | }
|
1839 | }
|
1840 |
|
1841 | if (value && typeof value === 'object') {
|
1842 | return fullyResolveKeys(/** @type {!Object} */ (value));
|
1843 | }
|
1844 |
|
1845 | if (typeof value === 'function') {
|
1846 | return fullyResolveKeys(/** @type {!Object} */ (value));
|
1847 | }
|
1848 |
|
1849 | return createPromise(resolve => resolve(value));
|
1850 | }
|
1851 |
|
1852 |
|
1853 | /**
|
1854 | * @param {!(Array|Object)} obj the object to resolve.
|
1855 | * @return {!Thenable} A promise that will be resolved with the
|
1856 | * input object once all of its values have been fully resolved.
|
1857 | */
|
1858 | function fullyResolveKeys(obj) {
|
1859 | var isArray = Array.isArray(obj);
|
1860 | var numKeys = isArray ? obj.length : (function() {
|
1861 | let n = 0;
|
1862 | for (let key in obj) {
|
1863 | n += 1;
|
1864 | }
|
1865 | return n;
|
1866 | })();
|
1867 |
|
1868 | if (!numKeys) {
|
1869 | return createPromise(resolve => resolve(obj));
|
1870 | }
|
1871 |
|
1872 | function forEachProperty(obj, fn) {
|
1873 | for (let key in obj) {
|
1874 | fn.call(null, obj[key], key, obj);
|
1875 | }
|
1876 | }
|
1877 |
|
1878 | function forEachElement(arr, fn) {
|
1879 | arr.forEach(fn);
|
1880 | }
|
1881 |
|
1882 | var numResolved = 0;
|
1883 | return createPromise(function(fulfill, reject) {
|
1884 | var forEachKey = isArray ? forEachElement: forEachProperty;
|
1885 |
|
1886 | forEachKey(obj, function(partialValue, key) {
|
1887 | if (!Array.isArray(partialValue)
|
1888 | && (!partialValue || typeof partialValue !== 'object')) {
|
1889 | maybeResolveValue();
|
1890 | return;
|
1891 | }
|
1892 |
|
1893 | fullyResolved(partialValue).then(
|
1894 | function(resolvedValue) {
|
1895 | obj[key] = resolvedValue;
|
1896 | maybeResolveValue();
|
1897 | },
|
1898 | reject);
|
1899 | });
|
1900 |
|
1901 | function maybeResolveValue() {
|
1902 | if (++numResolved == numKeys) {
|
1903 | fulfill(obj);
|
1904 | }
|
1905 | }
|
1906 | });
|
1907 | }
|
1908 |
|
1909 |
|
1910 | //////////////////////////////////////////////////////////////////////////////
|
1911 | //
|
1912 | // ControlFlow
|
1913 | //
|
1914 | //////////////////////////////////////////////////////////////////////////////
|
1915 |
|
1916 |
|
1917 | /**
|
1918 | * Defines methods for coordinating the execution of asynchronous tasks.
|
1919 | * @record
|
1920 | */
|
1921 | class Scheduler {
|
1922 | /**
|
1923 | * Schedules a task for execution. If the task function is a generator, the
|
1924 | * task will be executed using {@link ./promise.consume consume()}.
|
1925 | *
|
1926 | * @param {function(): (T|IThenable<T>)} fn The function to call to start the
|
1927 | * task.
|
1928 | * @param {string=} opt_description A description of the task for debugging
|
1929 | * purposes.
|
1930 | * @return {!Thenable<T>} A promise that will be resolved with the task
|
1931 | * result.
|
1932 | * @template T
|
1933 | */
|
1934 | execute(fn, opt_description) {}
|
1935 |
|
1936 | /**
|
1937 | * Creates a new promise using the given resolver function.
|
1938 | *
|
1939 | * @param {function(
|
1940 | * function((T|IThenable<T>|Thenable|null)=),
|
1941 | * function(*=))} resolver
|
1942 | * @return {!Thenable<T>}
|
1943 | * @template T
|
1944 | */
|
1945 | promise(resolver) {}
|
1946 |
|
1947 | /**
|
1948 | * Schedules a `setTimeout` call.
|
1949 | *
|
1950 | * @param {number} ms The timeout delay, in milliseconds.
|
1951 | * @param {string=} opt_description A description to accompany the timeout.
|
1952 | * @return {!Thenable<void>} A promise that will be resolved when the timeout
|
1953 | * fires.
|
1954 | */
|
1955 | timeout(ms, opt_description) {}
|
1956 |
|
1957 | /**
|
1958 | * Schedules a task to wait for a condition to hold.
|
1959 | *
|
1960 | * If the condition is defined as a function, it may return any value. Promies
|
1961 | * will be resolved before testing if the condition holds (resolution time
|
1962 | * counts towards the timeout). Once resolved, values are always evaluated as
|
1963 | * booleans.
|
1964 | *
|
1965 | * If the condition function throws, or returns a rejected promise, the
|
1966 | * wait task will fail.
|
1967 | *
|
1968 | * If the condition is defined as a promise, the scheduler will wait for it to
|
1969 | * settle. If the timeout expires before the promise settles, the promise
|
1970 | * returned by this function will be rejected.
|
1971 | *
|
1972 | * If this function is invoked with `timeout === 0`, or the timeout is
|
1973 | * omitted, this scheduler will wait indefinitely for the condition to be
|
1974 | * satisfied.
|
1975 | *
|
1976 | * @param {(!IThenable<T>|function())} condition The condition to poll,
|
1977 | * or a promise to wait on.
|
1978 | * @param {number=} opt_timeout How long to wait, in milliseconds, for the
|
1979 | * condition to hold before timing out. If omitted, the flow will wait
|
1980 | * indefinitely.
|
1981 | * @param {string=} opt_message An optional error message to include if the
|
1982 | * wait times out; defaults to the empty string.
|
1983 | * @return {!Thenable<T>} A promise that will be fulfilled
|
1984 | * when the condition has been satisified. The promise shall be rejected
|
1985 | * if the wait times out waiting for the condition.
|
1986 | * @throws {TypeError} If condition is not a function or promise or if timeout
|
1987 | * is not a number >= 0.
|
1988 | * @template T
|
1989 | */
|
1990 | wait(condition, opt_timeout, opt_message) {}
|
1991 | }
|
1992 |
|
1993 |
|
1994 | let USE_PROMISE_MANAGER;
|
1995 | function usePromiseManager() {
|
1996 | if (typeof USE_PROMISE_MANAGER !== 'undefined') {
|
1997 | return !!USE_PROMISE_MANAGER;
|
1998 | }
|
1999 | return process.env['SELENIUM_PROMISE_MANAGER'] === undefined
|
2000 | || !/^0|false$/i.test(process.env['SELENIUM_PROMISE_MANAGER']);
|
2001 | }
|
2002 |
|
2003 |
|
2004 | /**
|
2005 | * @param {function(
|
2006 | * function((T|IThenable<T>|Thenable|null)=),
|
2007 | * function(*=))} resolver
|
2008 | * @return {!Thenable<T>}
|
2009 | * @template T
|
2010 | */
|
2011 | function createPromise(resolver) {
|
2012 | let ctor = usePromiseManager() ? ManagedPromise : NativePromise;
|
2013 | return new ctor(resolver);
|
2014 | }
|
2015 |
|
2016 |
|
2017 | /**
|
2018 | * @param {!Scheduler} scheduler The scheduler to use.
|
2019 | * @param {(!IThenable<T>|function())} condition The condition to poll,
|
2020 | * or a promise to wait on.
|
2021 | * @param {number=} opt_timeout How long to wait, in milliseconds, for the
|
2022 | * condition to hold before timing out. If omitted, the flow will wait
|
2023 | * indefinitely.
|
2024 | * @param {string=} opt_message An optional error message to include if the
|
2025 | * wait times out; defaults to the empty string.
|
2026 | * @return {!Thenable<T>} A promise that will be fulfilled
|
2027 | * when the condition has been satisified. The promise shall be rejected
|
2028 | * if the wait times out waiting for the condition.
|
2029 | * @throws {TypeError} If condition is not a function or promise or if timeout
|
2030 | * is not a number >= 0.
|
2031 | * @template T
|
2032 | */
|
2033 | function scheduleWait(scheduler, condition, opt_timeout, opt_message) {
|
2034 | let timeout = opt_timeout || 0;
|
2035 | if (typeof timeout !== 'number' || timeout < 0) {
|
2036 | throw TypeError('timeout must be a number >= 0: ' + timeout);
|
2037 | }
|
2038 |
|
2039 | if (isPromise(condition)) {
|
2040 | return scheduler.execute(function() {
|
2041 | if (!timeout) {
|
2042 | return condition;
|
2043 | }
|
2044 | return scheduler.promise(function(fulfill, reject) {
|
2045 | let start = Date.now();
|
2046 | let timer = setTimeout(function() {
|
2047 | timer = null;
|
2048 | reject(Error((opt_message ? opt_message + '\n' : '') +
|
2049 | 'Timed out waiting for promise to resolve after ' +
|
2050 | (Date.now() - start) + 'ms'));
|
2051 | }, timeout);
|
2052 |
|
2053 | /** @type {Thenable} */(condition).then(
|
2054 | function(value) {
|
2055 | timer && clearTimeout(timer);
|
2056 | fulfill(value);
|
2057 | },
|
2058 | function(error) {
|
2059 | timer && clearTimeout(timer);
|
2060 | reject(error);
|
2061 | });
|
2062 | });
|
2063 | }, opt_message || '<anonymous wait: promise resolution>');
|
2064 | }
|
2065 |
|
2066 | if (typeof condition !== 'function') {
|
2067 | throw TypeError('Invalid condition; must be a function or promise: ' +
|
2068 | typeof condition);
|
2069 | }
|
2070 |
|
2071 | if (isGenerator(condition)) {
|
2072 | let original = condition;
|
2073 | condition = () => consume(original);
|
2074 | }
|
2075 |
|
2076 | return scheduler.execute(function() {
|
2077 | var startTime = Date.now();
|
2078 | return scheduler.promise(function(fulfill, reject) {
|
2079 | pollCondition();
|
2080 |
|
2081 | function pollCondition() {
|
2082 | var conditionFn = /** @type {function()} */(condition);
|
2083 | scheduler.execute(conditionFn).then(function(value) {
|
2084 | var elapsed = Date.now() - startTime;
|
2085 | if (!!value) {
|
2086 | fulfill(value);
|
2087 | } else if (timeout && elapsed >= timeout) {
|
2088 | reject(new Error((opt_message ? opt_message + '\n' : '') +
|
2089 | 'Wait timed out after ' + elapsed + 'ms'));
|
2090 | } else {
|
2091 | // Do not use asyncRun here because we need a non-micro yield
|
2092 | // here so the UI thread is given a chance when running in a
|
2093 | // browser.
|
2094 | setTimeout(pollCondition, 0);
|
2095 | }
|
2096 | }, reject);
|
2097 | }
|
2098 | });
|
2099 | }, opt_message || '<anonymous wait>');
|
2100 | }
|
2101 |
|
2102 |
|
2103 | /**
|
2104 | * A scheduler that executes all tasks immediately, with no coordination. This
|
2105 | * class is an event emitter for API compatibility with the {@link ControlFlow},
|
2106 | * however, it emits no events.
|
2107 | *
|
2108 | * @implements {Scheduler}
|
2109 | */
|
2110 | class SimpleScheduler extends events.EventEmitter {
|
2111 | /** @override */
|
2112 | execute(fn) {
|
2113 | return this.promise((resolve, reject) => {
|
2114 | try {
|
2115 | if (isGenerator(fn)) {
|
2116 | consume(fn).then(resolve, reject);
|
2117 | } else {
|
2118 | resolve(fn.call(undefined));
|
2119 | }
|
2120 | } catch (ex) {
|
2121 | reject(ex);
|
2122 | }
|
2123 | });
|
2124 | }
|
2125 |
|
2126 | /** @override */
|
2127 | promise(resolver) {
|
2128 | return new NativePromise(resolver);
|
2129 | }
|
2130 |
|
2131 | /** @override */
|
2132 | timeout(ms) {
|
2133 | return this.promise(resolve => setTimeout(_ => resolve(), ms));
|
2134 | }
|
2135 |
|
2136 | /** @override */
|
2137 | wait(condition, opt_timeout, opt_message) {
|
2138 | return scheduleWait(this, condition, opt_timeout, opt_message);
|
2139 | }
|
2140 | }
|
2141 | const SIMPLE_SCHEDULER = new SimpleScheduler;
|
2142 |
|
2143 |
|
2144 | /**
|
2145 | * Handles the execution of scheduled tasks, each of which may be an
|
2146 | * asynchronous operation. The control flow will ensure tasks are executed in
|
2147 | * the ordered scheduled, starting each task only once those before it have
|
2148 | * completed.
|
2149 | *
|
2150 | * Each task scheduled within this flow may return a {@link ManagedPromise} to
|
2151 | * indicate it is an asynchronous operation. The ControlFlow will wait for such
|
2152 | * promises to be resolved before marking the task as completed.
|
2153 | *
|
2154 | * Tasks and each callback registered on a {@link ManagedPromise} will be run
|
2155 | * in their own ControlFlow frame. Any tasks scheduled within a frame will take
|
2156 | * priority over previously scheduled tasks. Furthermore, if any of the tasks in
|
2157 | * the frame fail, the remainder of the tasks in that frame will be discarded
|
2158 | * and the failure will be propagated to the user through the callback/task's
|
2159 | * promised result.
|
2160 | *
|
2161 | * Each time a ControlFlow empties its task queue, it will fire an
|
2162 | * {@link ControlFlow.EventType.IDLE IDLE} event. Conversely,
|
2163 | * whenever the flow terminates due to an unhandled error, it will remove all
|
2164 | * remaining tasks in its queue and fire an
|
2165 | * {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION UNCAUGHT_EXCEPTION} event.
|
2166 | * If there are no listeners registered with the flow, the error will be
|
2167 | * rethrown to the global error handler.
|
2168 | *
|
2169 | * Refer to the {@link ./promise} module documentation for a detailed
|
2170 | * explanation of how the ControlFlow coordinates task execution.
|
2171 | *
|
2172 | * @implements {Scheduler}
|
2173 | * @final
|
2174 | */
|
2175 | class ControlFlow extends events.EventEmitter {
|
2176 | constructor() {
|
2177 | if (!usePromiseManager()) {
|
2178 | throw TypeError(
|
2179 | 'Cannot instantiate control flow when the promise manager has'
|
2180 | + ' been disabled');
|
2181 | }
|
2182 |
|
2183 | super();
|
2184 |
|
2185 | /** @private {boolean} */
|
2186 | this.propagateUnhandledRejections_ = true;
|
2187 |
|
2188 | /** @private {TaskQueue} */
|
2189 | this.activeQueue_ = null;
|
2190 |
|
2191 | /** @private {Set<TaskQueue>} */
|
2192 | this.taskQueues_ = null;
|
2193 |
|
2194 | /**
|
2195 | * Micro task that controls shutting down the control flow. Upon shut down,
|
2196 | * the flow will emit an
|
2197 | * {@link ControlFlow.EventType.IDLE} event. Idle events
|
2198 | * always follow a brief timeout in order to catch latent errors from the
|
2199 | * last completed task. If this task had a callback registered, but no
|
2200 | * errback, and the task fails, the unhandled failure would not be reported
|
2201 | * by the promise system until the next turn of the event loop:
|
2202 | *
|
2203 | * // Schedule 1 task that fails.
|
2204 | * var result = promise.controlFlow().schedule('example',
|
2205 | * function() { return promise.rejected('failed'); });
|
2206 | * // Set a callback on the result. This delays reporting the unhandled
|
2207 | * // failure for 1 turn of the event loop.
|
2208 | * result.then(function() {});
|
2209 | *
|
2210 | * @private {MicroTask}
|
2211 | */
|
2212 | this.shutdownTask_ = null;
|
2213 |
|
2214 | /**
|
2215 | * ID for a long running interval used to keep a Node.js process running
|
2216 | * while a control flow's event loop is still working. This is a cheap hack
|
2217 | * required since JS events are only scheduled to run when there is
|
2218 | * _actually_ something to run. When a control flow is waiting on a task,
|
2219 | * there will be nothing in the JS event loop and the process would
|
2220 | * terminate without this.
|
2221 | * @private
|
2222 | */
|
2223 | this.hold_ = null;
|
2224 | }
|
2225 |
|
2226 | /**
|
2227 | * Returns a string representation of this control flow, which is its current
|
2228 | * {@linkplain #getSchedule() schedule}, sans task stack traces.
|
2229 | * @return {string} The string representation of this contorl flow.
|
2230 | * @override
|
2231 | */
|
2232 | toString() {
|
2233 | return this.getSchedule();
|
2234 | }
|
2235 |
|
2236 | /**
|
2237 | * Sets whether any unhandled rejections should propagate up through the
|
2238 | * control flow stack and cause rejections within parent tasks. If error
|
2239 | * propagation is disabled, tasks will not be aborted when an unhandled
|
2240 | * promise rejection is detected, but the rejection _will_ trigger an
|
2241 | * {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION}
|
2242 | * event.
|
2243 | *
|
2244 | * The default behavior is to propagate all unhandled rejections. _The use
|
2245 | * of this option is highly discouraged._
|
2246 | *
|
2247 | * @param {boolean} propagate whether to propagate errors.
|
2248 | */
|
2249 | setPropagateUnhandledRejections(propagate) {
|
2250 | this.propagateUnhandledRejections_ = propagate;
|
2251 | }
|
2252 |
|
2253 | /**
|
2254 | * @return {boolean} Whether this flow is currently idle.
|
2255 | */
|
2256 | isIdle() {
|
2257 | return !this.shutdownTask_ && (!this.taskQueues_ || !this.taskQueues_.size);
|
2258 | }
|
2259 |
|
2260 | /**
|
2261 | * Resets this instance, clearing its queue and removing all event listeners.
|
2262 | */
|
2263 | reset() {
|
2264 | this.cancelQueues_(new FlowResetError);
|
2265 | this.emit(ControlFlow.EventType.RESET);
|
2266 | this.removeAllListeners();
|
2267 | this.cancelShutdown_();
|
2268 | }
|
2269 |
|
2270 | /**
|
2271 | * Generates an annotated string describing the internal state of this control
|
2272 | * flow, including the currently executing as well as pending tasks. If
|
2273 | * {@code opt_includeStackTraces === true}, the string will include the
|
2274 | * stack trace from when each task was scheduled.
|
2275 | * @param {string=} opt_includeStackTraces Whether to include the stack traces
|
2276 | * from when each task was scheduled. Defaults to false.
|
2277 | * @return {string} String representation of this flow's internal state.
|
2278 | */
|
2279 | getSchedule(opt_includeStackTraces) {
|
2280 | var ret = 'ControlFlow::' + getUid(this);
|
2281 | var activeQueue = this.activeQueue_;
|
2282 | if (!this.taskQueues_ || !this.taskQueues_.size) {
|
2283 | return ret;
|
2284 | }
|
2285 | var childIndent = '| ';
|
2286 | for (var q of this.taskQueues_) {
|
2287 | ret += '\n' + printQ(q, childIndent);
|
2288 | }
|
2289 | return ret;
|
2290 |
|
2291 | function printQ(q, indent) {
|
2292 | var ret = q.toString();
|
2293 | if (q === activeQueue) {
|
2294 | ret = '(active) ' + ret;
|
2295 | }
|
2296 | var prefix = indent + childIndent;
|
2297 | if (q.pending_) {
|
2298 | if (q.pending_.q.state_ !== TaskQueueState.FINISHED) {
|
2299 | ret += '\n' + prefix + '(pending) ' + q.pending_.task;
|
2300 | ret += '\n' + printQ(q.pending_.q, prefix + childIndent);
|
2301 | } else {
|
2302 | ret += '\n' + prefix + '(blocked) ' + q.pending_.task;
|
2303 | }
|
2304 | }
|
2305 | if (q.interrupts_) {
|
2306 | q.interrupts_.forEach((task) => {
|
2307 | ret += '\n' + prefix + task;
|
2308 | });
|
2309 | }
|
2310 | if (q.tasks_) {
|
2311 | q.tasks_.forEach((task) => ret += printTask(task, '\n' + prefix));
|
2312 | }
|
2313 | return indent + ret;
|
2314 | }
|
2315 |
|
2316 | function printTask(task, prefix) {
|
2317 | var ret = prefix + task;
|
2318 | if (opt_includeStackTraces && task.promise.stack_) {
|
2319 | ret += prefix + childIndent
|
2320 | + (task.promise.stack_.stack || task.promise.stack_)
|
2321 | .replace(/\n/g, prefix);
|
2322 | }
|
2323 | return ret;
|
2324 | }
|
2325 | }
|
2326 |
|
2327 | /**
|
2328 | * Returns the currently actively task queue for this flow. If there is no
|
2329 | * active queue, one will be created.
|
2330 | * @return {!TaskQueue} the currently active task queue for this flow.
|
2331 | * @private
|
2332 | */
|
2333 | getActiveQueue_() {
|
2334 | if (this.activeQueue_) {
|
2335 | return this.activeQueue_;
|
2336 | }
|
2337 |
|
2338 | this.activeQueue_ = new TaskQueue(this);
|
2339 | if (!this.taskQueues_) {
|
2340 | this.taskQueues_ = new Set();
|
2341 | }
|
2342 | this.taskQueues_.add(this.activeQueue_);
|
2343 | this.activeQueue_
|
2344 | .once('end', this.onQueueEnd_, this)
|
2345 | .once('error', this.onQueueError_, this);
|
2346 |
|
2347 | asyncRun(() => this.activeQueue_ = null);
|
2348 | this.activeQueue_.start();
|
2349 | return this.activeQueue_;
|
2350 | }
|
2351 |
|
2352 | /** @override */
|
2353 | execute(fn, opt_description) {
|
2354 | if (isGenerator(fn)) {
|
2355 | let original = fn;
|
2356 | fn = () => consume(original);
|
2357 | }
|
2358 |
|
2359 | if (!this.hold_) {
|
2360 | var holdIntervalMs = 2147483647; // 2^31-1; max timer length for Node.js
|
2361 | this.hold_ = setInterval(function() {}, holdIntervalMs);
|
2362 | }
|
2363 |
|
2364 | var task = new Task(
|
2365 | this, fn, opt_description || '<anonymous>',
|
2366 | {name: 'Task', top: ControlFlow.prototype.execute});
|
2367 |
|
2368 | var q = this.getActiveQueue_();
|
2369 | q.enqueue(task);
|
2370 | this.emit(ControlFlow.EventType.SCHEDULE_TASK, task.description);
|
2371 | return task.promise;
|
2372 | }
|
2373 |
|
2374 | /** @override */
|
2375 | promise(resolver) {
|
2376 | return new ManagedPromise(resolver, this);
|
2377 | }
|
2378 |
|
2379 | /** @override */
|
2380 | timeout(ms, opt_description) {
|
2381 | return this.execute(() => {
|
2382 | return this.promise(resolve => setTimeout(() => resolve(), ms));
|
2383 | }, opt_description);
|
2384 | }
|
2385 |
|
2386 | /** @override */
|
2387 | wait(condition, opt_timeout, opt_message) {
|
2388 | return scheduleWait(this, condition, opt_timeout, opt_message);
|
2389 | }
|
2390 |
|
2391 | /**
|
2392 | * Executes a function in the next available turn of the JavaScript event
|
2393 | * loop. This ensures the function runs with its own task queue and any
|
2394 | * scheduled tasks will run in "parallel" to those scheduled in the current
|
2395 | * function.
|
2396 | *
|
2397 | * flow.execute(() => console.log('a'));
|
2398 | * flow.execute(() => console.log('b'));
|
2399 | * flow.execute(() => console.log('c'));
|
2400 | * flow.async(() => {
|
2401 | * flow.execute(() => console.log('d'));
|
2402 | * flow.execute(() => console.log('e'));
|
2403 | * });
|
2404 | * flow.async(() => {
|
2405 | * flow.execute(() => console.log('f'));
|
2406 | * flow.execute(() => console.log('g'));
|
2407 | * });
|
2408 | * flow.once('idle', () => console.log('fin'));
|
2409 | * // a
|
2410 | * // d
|
2411 | * // f
|
2412 | * // b
|
2413 | * // e
|
2414 | * // g
|
2415 | * // c
|
2416 | * // fin
|
2417 | *
|
2418 | * If the function itself throws, the error will be treated the same as an
|
2419 | * unhandled rejection within the control flow.
|
2420 | *
|
2421 | * __NOTE__: This function is considered _unstable_.
|
2422 | *
|
2423 | * @param {!Function} fn The function to execute.
|
2424 | * @param {Object=} opt_self The object in whose context to run the function.
|
2425 | * @param {...*} var_args Any arguments to pass to the function.
|
2426 | */
|
2427 | async(fn, opt_self, var_args) {
|
2428 | asyncRun(() => {
|
2429 | // Clear any lingering queues, forces getActiveQueue_ to create a new one.
|
2430 | this.activeQueue_ = null;
|
2431 | var q = this.getActiveQueue_();
|
2432 | try {
|
2433 | q.execute_(fn.bind(opt_self, var_args));
|
2434 | } catch (ex) {
|
2435 | var cancellationError = CancellationError.wrap(ex,
|
2436 | 'Function passed to ControlFlow.async() threw');
|
2437 | cancellationError.silent_ = true;
|
2438 | q.abort_(cancellationError);
|
2439 | } finally {
|
2440 | this.activeQueue_ = null;
|
2441 | }
|
2442 | });
|
2443 | }
|
2444 |
|
2445 | /**
|
2446 | * Event handler for when a task queue is exhausted. This starts the shutdown
|
2447 | * sequence for this instance if there are no remaining task queues: after
|
2448 | * one turn of the event loop, this object will emit the
|
2449 | * {@link ControlFlow.EventType.IDLE IDLE} event to signal
|
2450 | * listeners that it has completed. During this wait, if another task is
|
2451 | * scheduled, the shutdown will be aborted.
|
2452 | *
|
2453 | * @param {!TaskQueue} q the completed task queue.
|
2454 | * @private
|
2455 | */
|
2456 | onQueueEnd_(q) {
|
2457 | if (!this.taskQueues_) {
|
2458 | return;
|
2459 | }
|
2460 | this.taskQueues_.delete(q);
|
2461 |
|
2462 | vlog(1, () => q + ' has finished');
|
2463 | vlog(1, () => this.taskQueues_.size + ' queues remain\n' + this, this);
|
2464 |
|
2465 | if (!this.taskQueues_.size) {
|
2466 | if (this.shutdownTask_) {
|
2467 | throw Error('Already have a shutdown task??');
|
2468 | }
|
2469 | vlog(1, () => 'Scheduling shutdown\n' + this);
|
2470 | this.shutdownTask_ = new MicroTask(() => this.shutdown_());
|
2471 | }
|
2472 | }
|
2473 |
|
2474 | /**
|
2475 | * Event handler for when a task queue terminates with an error. This triggers
|
2476 | * the cancellation of all other task queues and a
|
2477 | * {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION} event.
|
2478 | * If there are no error event listeners registered with this instance, the
|
2479 | * error will be rethrown to the global error handler.
|
2480 | *
|
2481 | * @param {*} error the error that caused the task queue to terminate.
|
2482 | * @param {!TaskQueue} q the task queue.
|
2483 | * @private
|
2484 | */
|
2485 | onQueueError_(error, q) {
|
2486 | if (this.taskQueues_) {
|
2487 | this.taskQueues_.delete(q);
|
2488 | }
|
2489 | this.cancelQueues_(CancellationError.wrap(
|
2490 | error, 'There was an uncaught error in the control flow'));
|
2491 | this.cancelShutdown_();
|
2492 | this.cancelHold_();
|
2493 |
|
2494 | setTimeout(() => {
|
2495 | let listeners = this.listeners(ControlFlow.EventType.UNCAUGHT_EXCEPTION);
|
2496 | if (!listeners.size) {
|
2497 | throw error;
|
2498 | } else {
|
2499 | this.reportUncaughtException_(error);
|
2500 | }
|
2501 | }, 0);
|
2502 | }
|
2503 |
|
2504 | /**
|
2505 | * Cancels all remaining task queues.
|
2506 | * @param {!CancellationError} reason The cancellation reason.
|
2507 | * @private
|
2508 | */
|
2509 | cancelQueues_(reason) {
|
2510 | reason.silent_ = true;
|
2511 | if (this.taskQueues_) {
|
2512 | for (var q of this.taskQueues_) {
|
2513 | q.removeAllListeners();
|
2514 | q.abort_(reason);
|
2515 | }
|
2516 | this.taskQueues_.clear();
|
2517 | this.taskQueues_ = null;
|
2518 | }
|
2519 | }
|
2520 |
|
2521 | /**
|
2522 | * Reports an uncaught exception using a
|
2523 | * {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION} event.
|
2524 | *
|
2525 | * @param {*} e the error to report.
|
2526 | * @private
|
2527 | */
|
2528 | reportUncaughtException_(e) {
|
2529 | this.emit(ControlFlow.EventType.UNCAUGHT_EXCEPTION, e);
|
2530 | }
|
2531 |
|
2532 | /** @private */
|
2533 | cancelHold_() {
|
2534 | if (this.hold_) {
|
2535 | clearInterval(this.hold_);
|
2536 | this.hold_ = null;
|
2537 | }
|
2538 | }
|
2539 |
|
2540 | /** @private */
|
2541 | shutdown_() {
|
2542 | vlog(1, () => 'Going idle: ' + this);
|
2543 | this.cancelHold_();
|
2544 | this.shutdownTask_ = null;
|
2545 | this.emit(ControlFlow.EventType.IDLE);
|
2546 | }
|
2547 |
|
2548 | /**
|
2549 | * Cancels the shutdown sequence if it is currently scheduled.
|
2550 | * @private
|
2551 | */
|
2552 | cancelShutdown_() {
|
2553 | if (this.shutdownTask_) {
|
2554 | this.shutdownTask_.cancel();
|
2555 | this.shutdownTask_ = null;
|
2556 | }
|
2557 | }
|
2558 | }
|
2559 |
|
2560 |
|
2561 | /**
|
2562 | * Events that may be emitted by an {@link ControlFlow}.
|
2563 | * @enum {string}
|
2564 | */
|
2565 | ControlFlow.EventType = {
|
2566 |
|
2567 | /** Emitted when all tasks have been successfully executed. */
|
2568 | IDLE: 'idle',
|
2569 |
|
2570 | /** Emitted when a ControlFlow has been reset. */
|
2571 | RESET: 'reset',
|
2572 |
|
2573 | /** Emitted whenever a new task has been scheduled. */
|
2574 | SCHEDULE_TASK: 'scheduleTask',
|
2575 |
|
2576 | /**
|
2577 | * Emitted whenever a control flow aborts due to an unhandled promise
|
2578 | * rejection. This event will be emitted along with the offending rejection
|
2579 | * reason. Upon emitting this event, the control flow will empty its task
|
2580 | * queue and revert to its initial state.
|
2581 | */
|
2582 | UNCAUGHT_EXCEPTION: 'uncaughtException'
|
2583 | };
|
2584 |
|
2585 |
|
2586 | /**
|
2587 | * Wraps a function to execute as a cancellable micro task.
|
2588 | * @final
|
2589 | */
|
2590 | class MicroTask {
|
2591 | /**
|
2592 | * @param {function()} fn The function to run as a micro task.
|
2593 | */
|
2594 | constructor(fn) {
|
2595 | /** @private {boolean} */
|
2596 | this.cancelled_ = false;
|
2597 | asyncRun(() => {
|
2598 | if (!this.cancelled_) {
|
2599 | fn();
|
2600 | }
|
2601 | });
|
2602 | }
|
2603 |
|
2604 | /**
|
2605 | * Runs the given function after a micro-task yield.
|
2606 | * @param {function()} fn The function to run.
|
2607 | */
|
2608 | static run(fn) {
|
2609 | NativePromise.resolve().then(function() {
|
2610 | try {
|
2611 | fn();
|
2612 | } catch (ignored) {
|
2613 | // Do nothing.
|
2614 | }
|
2615 | });
|
2616 | }
|
2617 |
|
2618 | /**
|
2619 | * Cancels the execution of this task. Note: this will not prevent the task
|
2620 | * timer from firing, just the invocation of the wrapped function.
|
2621 | */
|
2622 | cancel() {
|
2623 | this.cancelled_ = true;
|
2624 | }
|
2625 | }
|
2626 |
|
2627 |
|
2628 | /**
|
2629 | * A task to be executed by a {@link ControlFlow}.
|
2630 | *
|
2631 | * @template T
|
2632 | * @final
|
2633 | */
|
2634 | class Task extends Deferred {
|
2635 | /**
|
2636 | * @param {!ControlFlow} flow The flow this instances belongs
|
2637 | * to.
|
2638 | * @param {function(): (T|!ManagedPromise<T>)} fn The function to
|
2639 | * call when the task executes. If it returns a
|
2640 | * {@link ManagedPromise}, the flow will wait for it to be
|
2641 | * resolved before starting the next task.
|
2642 | * @param {string} description A description of the task for debugging.
|
2643 | * @param {{name: string, top: !Function}=} opt_stackOptions Options to use
|
2644 | * when capturing the stacktrace for when this task was created.
|
2645 | */
|
2646 | constructor(flow, fn, description, opt_stackOptions) {
|
2647 | super(flow);
|
2648 | getUid(this);
|
2649 |
|
2650 | /** @type {function(): (T|!ManagedPromise<T>)} */
|
2651 | this.execute = fn;
|
2652 |
|
2653 | /** @type {string} */
|
2654 | this.description = description;
|
2655 |
|
2656 | /** @type {TaskQueue} */
|
2657 | this.queue = null;
|
2658 |
|
2659 | /**
|
2660 | * Whether this task is considered block. A blocked task may be registered
|
2661 | * in a task queue, but will be dropped if it is still blocked when it
|
2662 | * reaches the front of the queue. A dropped task may always be rescheduled.
|
2663 | *
|
2664 | * Blocked tasks are used when a callback is attached to an unsettled
|
2665 | * promise to reserve a spot in line (in a manner of speaking). If the
|
2666 | * promise is not settled before the callback reaches the front of the
|
2667 | * of the queue, it will be dropped. Once the promise is settled, the
|
2668 | * dropped task will be rescheduled as an interrupt on the currently task
|
2669 | * queue.
|
2670 | *
|
2671 | * @type {boolean}
|
2672 | */
|
2673 | this.blocked = false;
|
2674 |
|
2675 | if (opt_stackOptions) {
|
2676 | this.promise.stack_ = captureStackTrace(
|
2677 | opt_stackOptions.name, this.description, opt_stackOptions.top);
|
2678 | }
|
2679 | }
|
2680 |
|
2681 | /** @override */
|
2682 | toString() {
|
2683 | return 'Task::' + getUid(this) + '<' + this.description + '>';
|
2684 | }
|
2685 | }
|
2686 |
|
2687 |
|
2688 | /** @enum {string} */
|
2689 | const TaskQueueState = {
|
2690 | NEW: 'new',
|
2691 | STARTED: 'started',
|
2692 | FINISHED: 'finished'
|
2693 | };
|
2694 |
|
2695 |
|
2696 | /**
|
2697 | * @final
|
2698 | */
|
2699 | class TaskQueue extends events.EventEmitter {
|
2700 | /** @param {!ControlFlow} flow . */
|
2701 | constructor(flow) {
|
2702 | super();
|
2703 |
|
2704 | /** @private {string} */
|
2705 | this.name_ = 'TaskQueue::' + getUid(this);
|
2706 |
|
2707 | /** @private {!ControlFlow} */
|
2708 | this.flow_ = flow;
|
2709 |
|
2710 | /** @private {!Array<!Task>} */
|
2711 | this.tasks_ = [];
|
2712 |
|
2713 | /** @private {Array<!Task>} */
|
2714 | this.interrupts_ = null;
|
2715 |
|
2716 | /** @private {({task: !Task, q: !TaskQueue}|null)} */
|
2717 | this.pending_ = null;
|
2718 |
|
2719 | /** @private {TaskQueue} */
|
2720 | this.subQ_ = null;
|
2721 |
|
2722 | /** @private {TaskQueueState} */
|
2723 | this.state_ = TaskQueueState.NEW;
|
2724 |
|
2725 | /** @private {!Set<!ManagedPromise>} */
|
2726 | this.unhandledRejections_ = new Set();
|
2727 | }
|
2728 |
|
2729 | /** @override */
|
2730 | toString() {
|
2731 | return 'TaskQueue::' + getUid(this);
|
2732 | }
|
2733 |
|
2734 | /**
|
2735 | * @param {!ManagedPromise} promise .
|
2736 | */
|
2737 | addUnhandledRejection(promise) {
|
2738 | // TODO: node 4.0.0+
|
2739 | vlog(2, () => this + ' registering unhandled rejection: ' + promise, this);
|
2740 | this.unhandledRejections_.add(promise);
|
2741 | }
|
2742 |
|
2743 | /**
|
2744 | * @param {!ManagedPromise} promise .
|
2745 | */
|
2746 | clearUnhandledRejection(promise) {
|
2747 | var deleted = this.unhandledRejections_.delete(promise);
|
2748 | if (deleted) {
|
2749 | // TODO: node 4.0.0+
|
2750 | vlog(2, () => this + ' clearing unhandled rejection: ' + promise, this);
|
2751 | }
|
2752 | }
|
2753 |
|
2754 | /**
|
2755 | * Enqueues a new task for execution.
|
2756 | * @param {!Task} task The task to enqueue.
|
2757 | * @throws {Error} If this instance has already started execution.
|
2758 | */
|
2759 | enqueue(task) {
|
2760 | if (this.state_ !== TaskQueueState.NEW) {
|
2761 | throw Error('TaskQueue has started: ' + this);
|
2762 | }
|
2763 |
|
2764 | if (task.queue) {
|
2765 | throw Error('Task is already scheduled in another queue');
|
2766 | }
|
2767 |
|
2768 | this.tasks_.push(task);
|
2769 | task.queue = this;
|
2770 | ON_CANCEL_HANDLER.set(
|
2771 | task.promise,
|
2772 | (e) => this.onTaskCancelled_(task, e));
|
2773 |
|
2774 | vlog(1, () => this + '.enqueue(' + task + ')', this);
|
2775 | vlog(2, () => this.flow_.toString(), this);
|
2776 | }
|
2777 |
|
2778 | /**
|
2779 | * Schedules the callbacks registered on the given promise in this queue.
|
2780 | *
|
2781 | * @param {!ManagedPromise} promise the promise whose callbacks should be
|
2782 | * registered as interrupts in this task queue.
|
2783 | * @throws {Error} if this queue has already finished.
|
2784 | */
|
2785 | scheduleCallbacks(promise) {
|
2786 | if (this.state_ === TaskQueueState.FINISHED) {
|
2787 | throw new Error('cannot interrupt a finished q(' + this + ')');
|
2788 | }
|
2789 |
|
2790 | if (this.pending_ && this.pending_.task.promise === promise) {
|
2791 | this.pending_.task.promise.queue_ = null;
|
2792 | this.pending_ = null;
|
2793 | asyncRun(() => this.executeNext_());
|
2794 | }
|
2795 |
|
2796 | if (!promise.callbacks_) {
|
2797 | return;
|
2798 | }
|
2799 | promise.callbacks_.forEach(function(cb) {
|
2800 | cb.blocked = false;
|
2801 | if (cb.queue) {
|
2802 | return;
|
2803 | }
|
2804 |
|
2805 | ON_CANCEL_HANDLER.set(
|
2806 | cb.promise,
|
2807 | (e) => this.onTaskCancelled_(cb, e));
|
2808 |
|
2809 | if (cb.queue === this && this.tasks_.indexOf(cb) !== -1) {
|
2810 | return;
|
2811 | }
|
2812 |
|
2813 | if (cb.queue) {
|
2814 | cb.queue.dropTask_(cb);
|
2815 | }
|
2816 |
|
2817 | cb.queue = this;
|
2818 | if (!this.interrupts_) {
|
2819 | this.interrupts_ = [];
|
2820 | }
|
2821 | this.interrupts_.push(cb);
|
2822 | }, this);
|
2823 | promise.callbacks_ = null;
|
2824 | vlog(2, () => this + ' interrupted\n' + this.flow_, this);
|
2825 | }
|
2826 |
|
2827 | /**
|
2828 | * Starts executing tasks in this queue. Once called, no further tasks may
|
2829 | * be {@linkplain #enqueue() enqueued} with this instance.
|
2830 | *
|
2831 | * @throws {Error} if this queue has already been started.
|
2832 | */
|
2833 | start() {
|
2834 | if (this.state_ !== TaskQueueState.NEW) {
|
2835 | throw new Error('TaskQueue has already started');
|
2836 | }
|
2837 | // Always asynchronously execute next, even if there doesn't look like
|
2838 | // there is anything in the queue. This will catch pending unhandled
|
2839 | // rejections that were registered before start was called.
|
2840 | asyncRun(() => this.executeNext_());
|
2841 | }
|
2842 |
|
2843 | /**
|
2844 | * Aborts this task queue. If there are any scheduled tasks, they are silently
|
2845 | * cancelled and discarded (their callbacks will never fire). If this queue
|
2846 | * has a _pending_ task, the abortion error is used to cancel that task.
|
2847 | * Otherwise, this queue will emit an error event.
|
2848 | *
|
2849 | * @param {*} error The abortion reason.
|
2850 | * @private
|
2851 | */
|
2852 | abort_(error) {
|
2853 | var cancellation;
|
2854 |
|
2855 | if (error instanceof FlowResetError) {
|
2856 | cancellation = error;
|
2857 | } else {
|
2858 | cancellation = new DiscardedTaskError(error);
|
2859 | }
|
2860 |
|
2861 | if (this.interrupts_ && this.interrupts_.length) {
|
2862 | this.interrupts_.forEach((t) => t.reject(cancellation));
|
2863 | this.interrupts_ = [];
|
2864 | }
|
2865 |
|
2866 | if (this.tasks_ && this.tasks_.length) {
|
2867 | this.tasks_.forEach((t) => t.reject(cancellation));
|
2868 | this.tasks_ = [];
|
2869 | }
|
2870 |
|
2871 | // Now that all of the remaining tasks have been silently cancelled (e.g. no
|
2872 | // exisitng callbacks on those tasks will fire), clear the silence bit on
|
2873 | // the cancellation error. This ensures additional callbacks registered in
|
2874 | // the future will actually execute.
|
2875 | cancellation.silent_ = false;
|
2876 |
|
2877 | if (this.pending_) {
|
2878 | vlog(2, () => this + '.abort(); cancelling pending task', this);
|
2879 | this.pending_.task.promise.cancel(
|
2880 | /** @type {!CancellationError} */(error));
|
2881 |
|
2882 | } else {
|
2883 | vlog(2, () => this + '.abort(); emitting error event', this);
|
2884 | this.emit('error', error, this);
|
2885 | }
|
2886 | }
|
2887 |
|
2888 | /** @private */
|
2889 | executeNext_() {
|
2890 | if (this.state_ === TaskQueueState.FINISHED) {
|
2891 | return;
|
2892 | }
|
2893 | this.state_ = TaskQueueState.STARTED;
|
2894 |
|
2895 | if (this.pending_ !== null || this.processUnhandledRejections_()) {
|
2896 | return;
|
2897 | }
|
2898 |
|
2899 | var task;
|
2900 | do {
|
2901 | task = this.getNextTask_();
|
2902 | } while (task && !isPending(task.promise));
|
2903 |
|
2904 | if (!task) {
|
2905 | this.state_ = TaskQueueState.FINISHED;
|
2906 | this.tasks_ = [];
|
2907 | this.interrupts_ = null;
|
2908 | vlog(2, () => this + '.emit(end)', this);
|
2909 | this.emit('end', this);
|
2910 | return;
|
2911 | }
|
2912 |
|
2913 | let result = undefined;
|
2914 | this.subQ_ = new TaskQueue(this.flow_);
|
2915 |
|
2916 | this.subQ_.once('end', () => { // On task completion.
|
2917 | this.subQ_ = null;
|
2918 | this.pending_ && this.pending_.task.fulfill(result);
|
2919 | });
|
2920 |
|
2921 | this.subQ_.once('error', e => { // On task failure.
|
2922 | this.subQ_ = null;
|
2923 | if (Thenable.isImplementation(result)) {
|
2924 | result.cancel(CancellationError.wrap(e));
|
2925 | }
|
2926 | this.pending_ && this.pending_.task.reject(e);
|
2927 | });
|
2928 | vlog(2, () => `${this} created ${this.subQ_} for ${task}`);
|
2929 |
|
2930 | try {
|
2931 | this.pending_ = {task: task, q: this.subQ_};
|
2932 | task.promise.queue_ = this;
|
2933 | result = this.subQ_.execute_(task.execute);
|
2934 | this.subQ_.start();
|
2935 | } catch (ex) {
|
2936 | this.subQ_.abort_(ex);
|
2937 | }
|
2938 | }
|
2939 |
|
2940 | /**
|
2941 | * @param {!Function} fn .
|
2942 | * @return {T} .
|
2943 | * @template T
|
2944 | * @private
|
2945 | */
|
2946 | execute_(fn) {
|
2947 | try {
|
2948 | activeFlows.push(this.flow_);
|
2949 | this.flow_.activeQueue_ = this;
|
2950 | return fn();
|
2951 | } finally {
|
2952 | this.flow_.activeQueue_ = null;
|
2953 | activeFlows.pop();
|
2954 | }
|
2955 | }
|
2956 |
|
2957 | /**
|
2958 | * Process any unhandled rejections registered with this task queue. If there
|
2959 | * is a rejection, this queue will be aborted with the rejection error. If
|
2960 | * there are multiple rejections registered, this queue will be aborted with
|
2961 | * a {@link MultipleUnhandledRejectionError}.
|
2962 | * @return {boolean} whether there was an unhandled rejection.
|
2963 | * @private
|
2964 | */
|
2965 | processUnhandledRejections_() {
|
2966 | if (!this.unhandledRejections_.size) {
|
2967 | return false;
|
2968 | }
|
2969 |
|
2970 | var errors = new Set();
|
2971 | for (var rejection of this.unhandledRejections_) {
|
2972 | errors.add(rejection.value_);
|
2973 | }
|
2974 | this.unhandledRejections_.clear();
|
2975 |
|
2976 | var errorToReport = errors.size === 1
|
2977 | ? errors.values().next().value
|
2978 | : new MultipleUnhandledRejectionError(errors);
|
2979 |
|
2980 | vlog(1, () => this + ' aborting due to unhandled rejections', this);
|
2981 | if (this.flow_.propagateUnhandledRejections_) {
|
2982 | this.abort_(errorToReport);
|
2983 | return true;
|
2984 | } else {
|
2985 | vlog(1, 'error propagation disabled; reporting to control flow');
|
2986 | this.flow_.reportUncaughtException_(errorToReport);
|
2987 | return false;
|
2988 | }
|
2989 | }
|
2990 |
|
2991 | /**
|
2992 | * @param {!Task} task The task to drop.
|
2993 | * @private
|
2994 | */
|
2995 | dropTask_(task) {
|
2996 | var index;
|
2997 | if (this.interrupts_) {
|
2998 | index = this.interrupts_.indexOf(task);
|
2999 | if (index != -1) {
|
3000 | task.queue = null;
|
3001 | this.interrupts_.splice(index, 1);
|
3002 | return;
|
3003 | }
|
3004 | }
|
3005 |
|
3006 | index = this.tasks_.indexOf(task);
|
3007 | if (index != -1) {
|
3008 | task.queue = null;
|
3009 | this.tasks_.splice(index, 1);
|
3010 | }
|
3011 | }
|
3012 |
|
3013 | /**
|
3014 | * @param {!Task} task The task that was cancelled.
|
3015 | * @param {!CancellationError} reason The cancellation reason.
|
3016 | * @private
|
3017 | */
|
3018 | onTaskCancelled_(task, reason) {
|
3019 | if (this.pending_ && this.pending_.task === task) {
|
3020 | this.pending_.q.abort_(reason);
|
3021 | } else {
|
3022 | this.dropTask_(task);
|
3023 | }
|
3024 | }
|
3025 |
|
3026 | /**
|
3027 | * @return {(Task|undefined)} the next task scheduled within this queue,
|
3028 | * if any.
|
3029 | * @private
|
3030 | */
|
3031 | getNextTask_() {
|
3032 | var task = undefined;
|
3033 | while (true) {
|
3034 | if (this.interrupts_) {
|
3035 | task = this.interrupts_.shift();
|
3036 | }
|
3037 | if (!task && this.tasks_) {
|
3038 | task = this.tasks_.shift();
|
3039 | }
|
3040 | if (task && task.blocked) {
|
3041 | vlog(2, () => this + ' skipping blocked task ' + task, this);
|
3042 | task.queue = null;
|
3043 | task = null;
|
3044 | // TODO: recurse when tail-call optimization is available in node.
|
3045 | } else {
|
3046 | break;
|
3047 | }
|
3048 | }
|
3049 | return task;
|
3050 | }
|
3051 | };
|
3052 |
|
3053 |
|
3054 |
|
3055 | /**
|
3056 | * The default flow to use if no others are active.
|
3057 | * @type {ControlFlow}
|
3058 | */
|
3059 | var defaultFlow;
|
3060 |
|
3061 |
|
3062 | /**
|
3063 | * A stack of active control flows, with the top of the stack used to schedule
|
3064 | * commands. When there are multiple flows on the stack, the flow at index N
|
3065 | * represents a callback triggered within a task owned by the flow at index
|
3066 | * N-1.
|
3067 | * @type {!Array<!ControlFlow>}
|
3068 | */
|
3069 | var activeFlows = [];
|
3070 |
|
3071 |
|
3072 | /**
|
3073 | * Changes the default flow to use when no others are active.
|
3074 | * @param {!ControlFlow} flow The new default flow.
|
3075 | * @throws {Error} If the default flow is not currently active.
|
3076 | */
|
3077 | function setDefaultFlow(flow) {
|
3078 | if (!usePromiseManager()) {
|
3079 | throw Error(
|
3080 | 'You may not change set the control flow when the promise'
|
3081 | +' manager is disabled');
|
3082 | }
|
3083 | if (activeFlows.length) {
|
3084 | throw Error('You may only change the default flow while it is active');
|
3085 | }
|
3086 | defaultFlow = flow;
|
3087 | }
|
3088 |
|
3089 |
|
3090 | /**
|
3091 | * @return {!ControlFlow} The currently active control flow.
|
3092 | * @suppress {checkTypes}
|
3093 | */
|
3094 | function controlFlow() {
|
3095 | if (!usePromiseManager()) {
|
3096 | return SIMPLE_SCHEDULER;
|
3097 | }
|
3098 |
|
3099 | if (activeFlows.length) {
|
3100 | return activeFlows[activeFlows.length - 1];
|
3101 | }
|
3102 |
|
3103 | if (!defaultFlow) {
|
3104 | defaultFlow = new ControlFlow;
|
3105 | }
|
3106 | return defaultFlow;
|
3107 | }
|
3108 |
|
3109 |
|
3110 | /**
|
3111 | * Creates a new control flow. The provided callback will be invoked as the
|
3112 | * first task within the new flow, with the flow as its sole argument. Returns
|
3113 | * a promise that resolves to the callback result.
|
3114 | * @param {function(!ControlFlow)} callback The entry point
|
3115 | * to the newly created flow.
|
3116 | * @return {!Thenable} A promise that resolves to the callback result.
|
3117 | */
|
3118 | function createFlow(callback) {
|
3119 | var flow = new ControlFlow;
|
3120 | return flow.execute(function() {
|
3121 | return callback(flow);
|
3122 | });
|
3123 | }
|
3124 |
|
3125 |
|
3126 | /**
|
3127 | * Tests is a function is a generator.
|
3128 | * @param {!Function} fn The function to test.
|
3129 | * @return {boolean} Whether the function is a generator.
|
3130 | */
|
3131 | function isGenerator(fn) {
|
3132 | return fn.constructor.name === 'GeneratorFunction';
|
3133 | }
|
3134 |
|
3135 |
|
3136 | /**
|
3137 | * Consumes a {@code GeneratorFunction}. Each time the generator yields a
|
3138 | * promise, this function will wait for it to be fulfilled before feeding the
|
3139 | * fulfilled value back into {@code next}. Likewise, if a yielded promise is
|
3140 | * rejected, the rejection error will be passed to {@code throw}.
|
3141 | *
|
3142 | * __Example 1:__ the Fibonacci Sequence.
|
3143 | *
|
3144 | * promise.consume(function* fibonacci() {
|
3145 | * var n1 = 1, n2 = 1;
|
3146 | * for (var i = 0; i < 4; ++i) {
|
3147 | * var tmp = yield n1 + n2;
|
3148 | * n1 = n2;
|
3149 | * n2 = tmp;
|
3150 | * }
|
3151 | * return n1 + n2;
|
3152 | * }).then(function(result) {
|
3153 | * console.log(result); // 13
|
3154 | * });
|
3155 | *
|
3156 | * __Example 2:__ a generator that throws.
|
3157 | *
|
3158 | * promise.consume(function* () {
|
3159 | * yield promise.delayed(250).then(function() {
|
3160 | * throw Error('boom');
|
3161 | * });
|
3162 | * }).catch(function(e) {
|
3163 | * console.log(e.toString()); // Error: boom
|
3164 | * });
|
3165 | *
|
3166 | * @param {!Function} generatorFn The generator function to execute.
|
3167 | * @param {Object=} opt_self The object to use as "this" when invoking the
|
3168 | * initial generator.
|
3169 | * @param {...*} var_args Any arguments to pass to the initial generator.
|
3170 | * @return {!Thenable<?>} A promise that will resolve to the
|
3171 | * generator's final result.
|
3172 | * @throws {TypeError} If the given function is not a generator.
|
3173 | */
|
3174 | function consume(generatorFn, opt_self, ...var_args) {
|
3175 | if (!isGenerator(generatorFn)) {
|
3176 | throw new TypeError('Input is not a GeneratorFunction: ' +
|
3177 | generatorFn.constructor.name);
|
3178 | }
|
3179 |
|
3180 | let ret;
|
3181 | return ret = createPromise((resolve, reject) => {
|
3182 | let generator = generatorFn.apply(opt_self, var_args);
|
3183 | callNext();
|
3184 |
|
3185 | /** @param {*=} opt_value . */
|
3186 | function callNext(opt_value) {
|
3187 | pump(generator.next, opt_value);
|
3188 | }
|
3189 |
|
3190 | /** @param {*=} opt_error . */
|
3191 | function callThrow(opt_error) {
|
3192 | pump(generator.throw, opt_error);
|
3193 | }
|
3194 |
|
3195 | function pump(fn, opt_arg) {
|
3196 | if (ret instanceof ManagedPromise && !isPending(ret)) {
|
3197 | return; // Defererd was cancelled; silently abort.
|
3198 | }
|
3199 |
|
3200 | try {
|
3201 | var result = fn.call(generator, opt_arg);
|
3202 | } catch (ex) {
|
3203 | reject(ex);
|
3204 | return;
|
3205 | }
|
3206 |
|
3207 | if (result.done) {
|
3208 | resolve(result.value);
|
3209 | return;
|
3210 | }
|
3211 |
|
3212 | asap(result.value, callNext, callThrow);
|
3213 | }
|
3214 | });
|
3215 | }
|
3216 |
|
3217 |
|
3218 | // PUBLIC API
|
3219 |
|
3220 |
|
3221 | module.exports = {
|
3222 | CancellableThenable: CancellableThenable,
|
3223 | CancellationError: CancellationError,
|
3224 | ControlFlow: ControlFlow,
|
3225 | Deferred: Deferred,
|
3226 | MultipleUnhandledRejectionError: MultipleUnhandledRejectionError,
|
3227 | Thenable: Thenable,
|
3228 | Promise: ManagedPromise,
|
3229 | Scheduler: Scheduler,
|
3230 | all: all,
|
3231 | asap: asap,
|
3232 | captureStackTrace: captureStackTrace,
|
3233 | checkedNodeCall: checkedNodeCall,
|
3234 | consume: consume,
|
3235 | controlFlow: controlFlow,
|
3236 | createFlow: createFlow,
|
3237 | defer: defer,
|
3238 | delayed: delayed,
|
3239 | filter: filter,
|
3240 | finally: thenFinally,
|
3241 | fulfilled: fulfilled,
|
3242 | fullyResolved: fullyResolved,
|
3243 | isGenerator: isGenerator,
|
3244 | isPromise: isPromise,
|
3245 | map: map,
|
3246 | rejected: rejected,
|
3247 | setDefaultFlow: setDefaultFlow,
|
3248 | when: when,
|
3249 |
|
3250 | /**
|
3251 | * Indicates whether the promise manager is currently enabled. When disabled,
|
3252 | * attempting to use the {@link ControlFlow} or {@link ManagedPromise Promise}
|
3253 | * classes will generate an error.
|
3254 | *
|
3255 | * The promise manager is currently enabled by default, but may be disabled
|
3256 | * by setting the environment variable `SELENIUM_PROMISE_MANAGER=0` or by
|
3257 | * setting this property to false. Setting this property will always take
|
3258 | * precedence ove the use of the environment variable.
|
3259 | *
|
3260 | * @return {boolean} Whether the promise manager is enabled.
|
3261 | * @see <https://github.com/SeleniumHQ/selenium/issues/2969>
|
3262 | */
|
3263 | get USE_PROMISE_MANAGER() { return usePromiseManager(); },
|
3264 | set USE_PROMISE_MANAGER(/** boolean */value) { USE_PROMISE_MANAGER = value; },
|
3265 |
|
3266 | get LONG_STACK_TRACES() { return LONG_STACK_TRACES; },
|
3267 | set LONG_STACK_TRACES(v) { LONG_STACK_TRACES = v; },
|
3268 | };
|