// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview
*
* > ### IMPORTANT NOTICE
* >
* > The promise manager contained in this module is in the process of being
* > phased out in favor of native JavaScript promises. This will be a long
* > process and will not be completed until there have been two major LTS Node
* > releases (approx. Node v10.0) that support
* > [async functions](https://tc39.github.io/ecmascript-asyncawait/).
* >
* > At this time, the promise manager can be disabled by setting an environment
* > variable, `SELENIUM_PROMISE_MANAGER=0`. In the absence of async functions,
* > users may use generators with the
* > {@link ./promise.consume promise.consume()} function to write "synchronous"
* > style tests:
* >
* > ```js
* > const {Builder, By, promise, until} = require('selenium-webdriver');
* >
* > let result = promise.consume(function* doGoogleSearch() {
* > let driver = new Builder().forBrowser('firefox').build();
* > yield driver.get('http://www.google.com/ncr');
* > yield driver.findElement(By.name('q')).sendKeys('webdriver');
* > yield driver.findElement(By.name('btnG')).click();
* > yield driver.wait(until.titleIs('webdriver - Google Search'), 1000);
* > yield driver.quit();
* > });
* >
* > result.then(_ => console.log('SUCCESS!'),
* > e => console.error('FAILURE: ' + e));
* > ```
* >
* > The motiviation behind this change and full deprecation plan are documented
* > in [issue 2969](https://github.com/SeleniumHQ/selenium/issues/2969).
* >
* >
*
* The promise module is centered around the {@linkplain ControlFlow}, a class
* that coordinates the execution of asynchronous tasks. The ControlFlow allows
* users to focus on the imperative commands for their script without worrying
* about chaining together every single asynchronous action, which can be
* tedious and verbose. APIs may be layered on top of the control flow to read
* as if they were synchronous. For instance, the core
* {@linkplain ./webdriver.WebDriver WebDriver} API is built on top of the
* control flow, allowing users to write
*
* driver.get('http://www.google.com/ncr');
* driver.findElement({name: 'q'}).sendKeys('webdriver');
* driver.findElement({name: 'btnGn'}).click();
*
* instead of
*
* driver.get('http://www.google.com/ncr')
* .then(function() {
* return driver.findElement({name: 'q'});
* })
* .then(function(q) {
* return q.sendKeys('webdriver');
* })
* .then(function() {
* return driver.findElement({name: 'btnG'});
* })
* .then(function(btnG) {
* return btnG.click();
* });
*
* ## Tasks and Task Queues
*
* The control flow is based on the concept of tasks and task queues. Tasks are
* functions that define the basic unit of work for the control flow to execute.
* Each task is scheduled via {@link ControlFlow#execute()}, which will return
* a {@link ManagedPromise ManagedPromise} that will be resolved with the task's
* result.
*
* A task queue contains all of the tasks scheduled within a single turn of the
* [JavaScript event loop][JSEL]. The control flow will create a new task queue
* the first time a task is scheduled within an event loop.
*
* var flow = promise.controlFlow();
* flow.execute(foo); // Creates a new task queue and inserts foo.
* flow.execute(bar); // Inserts bar into the same queue as foo.
* setTimeout(function() {
* flow.execute(baz); // Creates a new task queue and inserts baz.
* }, 0);
*
* Whenever the control flow creates a new task queue, it will automatically
* begin executing tasks in the next available turn of the event loop. This
* execution is scheduled using a "micro-task" timer, such as a (native)
* `ManagedPromise.then()` callback.
*
* setTimeout(() => console.log('a'));
* ManagedPromise.resolve().then(() => console.log('b')); // A native promise.
* flow.execute(() => console.log('c'));
* ManagedPromise.resolve().then(() => console.log('d'));
* setTimeout(() => console.log('fin'));
* // b
* // c
* // d
* // a
* // fin
*
* In the example above, b/c/d is logged before a/fin because native promises
* and this module use "micro-task" timers, which have a higher priority than
* "macro-tasks" like `setTimeout`.
*
* ## Task Execution
*
* Upon creating a task queue, and whenever an exisiting queue completes a task,
* the control flow will schedule a micro-task timer to process any scheduled
* tasks. This ensures no task is ever started within the same turn of the
* JavaScript event loop in which it was scheduled, nor is a task ever started
* within the same turn that another finishes.
*
* When the execution timer fires, a single task will be dequeued and executed.
* There are several important events that may occur while executing a task
* function:
*
* 1. A new task queue is created by a call to {@link ControlFlow#execute()}.
* Any tasks scheduled within this task queue are considered subtasks of the
* current task.
* 2. The task function throws an error. Any scheduled tasks are immediately
* discarded and the task's promised result (previously returned by
* {@link ControlFlow#execute()}) is immediately rejected with the thrown
* error.
* 3. The task function returns sucessfully.
*
* If a task function created a new task queue, the control flow will wait for
* that queue to complete before processing the task result. If the queue
* completes without error, the flow will settle the task's promise with the
* value originaly returned by the task function. On the other hand, if the task
* queue termintes with an error, the task's promise will be rejected with that
* error.
*
* flow.execute(function() {
* flow.execute(() => console.log('a'));
* flow.execute(() => console.log('b'));
* });
* flow.execute(() => console.log('c'));
* // a
* // b
* // c
*
* ## ManagedPromise Integration
*
* In addition to the {@link ControlFlow} class, the promise module also exports
* a [ManagedPromise/A+] {@linkplain ManagedPromise implementation} that is deeply
* integrated with the ControlFlow. First and foremost, each promise
* {@linkplain ManagedPromise#then() callback} is scheduled with the
* control flow as a task. As a result, each callback is invoked in its own turn
* of the JavaScript event loop with its own task queue. If any tasks are
* scheduled within a callback, the callback's promised result will not be
* settled until the task queue has completed.
*
* promise.fulfilled().then(function() {
* flow.execute(function() {
* console.log('b');
* });
* }).then(() => console.log('a'));
* // b
* // a
*
* ### Scheduling ManagedPromise Callbacks
*
* How callbacks are scheduled in the control flow depends on when they are
* attached to the promise. Callbacks attached to a _previously_ resolved
* promise are immediately enqueued as subtasks of the currently running task.
*
* var p = promise.fulfilled();
* flow.execute(function() {
* flow.execute(() => console.log('A'));
* p.then( () => console.log('B'));
* flow.execute(() => console.log('C'));
* p.then( () => console.log('D'));
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // fin
*
* When a promise is resolved while a task function is on the call stack, any
* callbacks also registered in that stack frame are scheduled as if the promise
* were already resolved:
*
* var d = promise.defer();
* flow.execute(function() {
* flow.execute( () => console.log('A'));
* d.promise.then(() => console.log('B'));
* flow.execute( () => console.log('C'));
* d.promise.then(() => console.log('D'));
*
* d.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // fin
*
* Callbacks attached to an _unresolved_ promise within a task function are
* only weakly scheduled as subtasks and will be dropped if they reach the
* front of the queue before the promise is resolved. In the example below, the
* callbacks for `B` & `D` are dropped as sub-tasks since they are attached to
* an unresolved promise when they reach the front of the task queue.
*
* var d = promise.defer();
* flow.execute(function() {
* flow.execute( () => console.log('A'));
* d.promise.then(() => console.log('B'));
* flow.execute( () => console.log('C'));
* d.promise.then(() => console.log('D'));
*
* setTimeout(d.fulfill, 20);
* }).then(function() {
* console.log('fin')
* });
* // A
* // C
* // fin
* // B
* // D
*
* If a promise is resolved while a task function is on the call stack, any
* previously registered and unqueued callbacks (i.e. either attached while no
* task was on the call stack, or previously dropped as described above) act as
* _interrupts_ and are inserted at the front of the task queue. If multiple
* promises are fulfilled, their interrupts are enqueued in the order the
* promises are resolved.
*
* var d1 = promise.defer();
* d1.promise.then(() => console.log('A'));
*
* var d2 = promise.defer();
* d2.promise.then(() => console.log('B'));
*
* flow.execute(function() {
* d1.promise.then(() => console.log('C'));
* flow.execute(() => console.log('D'));
* });
* flow.execute(function() {
* flow.execute(() => console.log('E'));
* flow.execute(() => console.log('F'));
* d1.fulfill();
* d2.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // D
* // A
* // C
* // B
* // E
* // F
* // fin
*
* Within a task function (or callback), each step of a promise chain acts as
* an interrupt on the task queue:
*
* var d = promise.defer();
* flow.execute(function() {
* d.promise.
* then(() => console.log('A')).
* then(() => console.log('B')).
* then(() => console.log('C')).
* then(() => console.log('D'));
*
* flow.execute(() => console.log('E'));
* d.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // E
* // fin
*
* If there are multiple promise chains derived from a single promise, they are
* processed in the order created:
*
* var d = promise.defer();
* flow.execute(function() {
* var chain = d.promise.then(() => console.log('A'));
*
* chain.then(() => console.log('B')).
* then(() => console.log('C'));
*
* chain.then(() => console.log('D')).
* then(() => console.log('E'));
*
* flow.execute(() => console.log('F'));
*
* d.fulfill();
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // E
* // F
* // fin
*
* Even though a subtask's promised result will never resolve while the task
* function is on the stack, it will be treated as a promise resolved within the
* task. In all other scenarios, a task's promise behaves just like a normal
* promise. In the sample below, `C/D` is loggged before `B` because the
* resolution of `subtask1` interrupts the flow of the enclosing task. Within
* the final subtask, `E/F` is logged in order because `subtask1` is a resolved
* promise when that task runs.
*
* flow.execute(function() {
* var subtask1 = flow.execute(() => console.log('A'));
* var subtask2 = flow.execute(() => console.log('B'));
*
* subtask1.then(() => console.log('C'));
* subtask1.then(() => console.log('D'));
*
* flow.execute(function() {
* flow.execute(() => console.log('E'));
* subtask1.then(() => console.log('F'));
* });
* }).then(function() {
* console.log('fin');
* });
* // A
* // C
* // D
* // B
* // E
* // F
* // fin
*
* Finally, consider the following:
*
* var d = promise.defer();
* d.promise.then(() => console.log('A'));
* d.promise.then(() => console.log('B'));
*
* flow.execute(function() {
* flow.execute( () => console.log('C'));
* d.promise.then(() => console.log('D'));
*
* flow.execute( () => console.log('E'));
* d.promise.then(() => console.log('F'));
*
* d.fulfill();
*
* flow.execute( () => console.log('G'));
* d.promise.then(() => console.log('H'));
* }).then(function() {
* console.log('fin');
* });
* // A
* // B
* // C
* // D
* // E
* // F
* // G
* // H
* // fin
*
* In this example, callbacks are registered on `d.promise` both before and
* during the invocation of the task function. When `d.fulfill()` is called,
* the callbacks registered before the task (`A` & `B`) are registered as
* interrupts. The remaining callbacks were all attached within the task and
* are scheduled in the flow as standard tasks.
*
* ## Generator Support
*
* [Generators][GF] may be scheduled as tasks within a control flow or attached
* as callbacks to a promise. Each time the generator yields a promise, the
* control flow will wait for that promise to settle before executing the next
* iteration of the generator. The yielded promise's fulfilled value will be
* passed back into the generator:
*
* flow.execute(function* () {
* var d = promise.defer();
*
* setTimeout(() => console.log('...waiting...'), 25);
* setTimeout(() => d.fulfill(123), 50);
*
* console.log('start: ' + Date.now());
*
* var value = yield d.promise;
* console.log('mid: %d; value = %d', Date.now(), value);
*
* yield promise.delayed(10);
* console.log('end: ' + Date.now());
* }).then(function() {
* console.log('fin');
* });
* // start: 0
* // ...waiting...
* // mid: 50; value = 123
* // end: 60
* // fin
*
* Yielding the result of a promise chain will wait for the entire chain to
* complete:
*
* promise.fulfilled().then(function* () {
* console.log('start: ' + Date.now());
*
* var value = yield flow.
* execute(() => console.log('A')).
* then( () => console.log('B')).
* then( () => 123);
*
* console.log('mid: %s; value = %d', Date.now(), value);
*
* yield flow.execute(() => console.log('C'));
* }).then(function() {
* console.log('fin');
* });
* // start: 0
* // A
* // B
* // mid: 2; value = 123
* // C
* // fin
*
* Yielding a _rejected_ promise will cause the rejected value to be thrown
* within the generator function:
*
* flow.execute(function* () {
* console.log('start: ' + Date.now());
* try {
* yield promise.delayed(10).then(function() {
* throw Error('boom');
* });
* } catch (ex) {
* console.log('caught time: ' + Date.now());
* console.log(ex.message);
* }
* });
* // start: 0
* // caught time: 10
* // boom
*
* # Error Handling
*
* ES6 promises do not require users to handle a promise rejections. This can
* result in subtle bugs as the rejections are silently "swallowed" by the
* ManagedPromise class.
*
* ManagedPromise.reject(Error('boom'));
* // ... *crickets* ...
*
* Selenium's promise module, on the other hand, requires that every rejection
* be explicitly handled. When a {@linkplain ManagedPromise ManagedPromise} is
* rejected and no callbacks are defined on that promise, it is considered an
* _unhandled rejection_ and reproted to the active task queue. If the rejection
* remains unhandled after a single turn of the [event loop][JSEL] (scheduled
* with a micro-task), it will propagate up the stack.
*
* ## Error Propagation
*
* If an unhandled rejection occurs within a task function, that task's promised
* result is rejected and all remaining subtasks are discarded:
*
* flow.execute(function() {
* // No callbacks registered on promise -> unhandled rejection
* promise.rejected(Error('boom'));
* flow.execute(function() { console.log('this will never run'); });
* }).catch(function(e) {
* console.log(e.message);
* });
* // boom
*
* The promised results for discarded tasks are silently rejected with a
* cancellation error and existing callback chains will never fire.
*
* flow.execute(function() {
* promise.rejected(Error('boom'));
* flow.execute(function() { console.log('a'); }).
* then(function() { console.log('b'); });
* }).catch(function(e) {
* console.log(e.message);
* });
* // boom
*
* An unhandled rejection takes precedence over a task function's returned
* result, even if that value is another promise:
*
* flow.execute(function() {
* promise.rejected(Error('boom'));
* return flow.execute(someOtherTask);
* }).catch(function(e) {
* console.log(e.message);
* });
* // boom
*
* If there are multiple unhandled rejections within a task, they are packaged
* in a {@link MultipleUnhandledRejectionError}, which has an `errors` property
* that is a `Set` of the recorded unhandled rejections:
*
* flow.execute(function() {
* promise.rejected(Error('boom1'));
* promise.rejected(Error('boom2'));
* }).catch(function(ex) {
* console.log(ex instanceof MultipleUnhandledRejectionError);
* for (var e of ex.errors) {
* console.log(e.message);
* }
* });
* // boom1
* // boom2
*
* When a subtask is discarded due to an unreported rejection in its parent
* frame, the existing callbacks on that task will never settle and the
* callbacks will not be invoked. If a new callback is attached ot the subtask
* _after_ it has been discarded, it is handled the same as adding a callback
* to a cancelled promise: the error-callback path is invoked. This behavior is
* intended to handle cases where the user saves a reference to a task promise,
* as illustrated below.
*
* var subTask;
* flow.execute(function() {
* promise.rejected(Error('boom'));
* subTask = flow.execute(function() {});
* }).catch(function(e) {
* console.log(e.message);
* }).then(function() {
* return subTask.then(
* () => console.log('subtask success!'),
* (e) => console.log('subtask failed:\n' + e));
* });
* // boom
* // subtask failed:
* // DiscardedTaskError: Task was discarded due to a previous failure: boom
*
* When a subtask fails, its promised result is treated the same as any other
* promise: it must be handled within one turn of the rejection or the unhandled
* rejection is propagated to the parent task. This means users can catch errors
* from complex flows from the top level task:
*
* flow.execute(function() {
* flow.execute(function() {
* flow.execute(function() {
* throw Error('fail!');
* });
* });
* }).catch(function(e) {
* console.log(e.message);
* });
* // fail!
*
* ## Unhandled Rejection Events
*
* When an unhandled rejection propagates to the root of the control flow, the
* flow will emit an __uncaughtException__ event. If no listeners are registered
* on the flow, the error will be rethrown to the global error handler: an
* __uncaughtException__ event from the
* [`process`](https://nodejs.org/api/process.html) object in node, or
* `window.onerror` when running in a browser.
*
* Bottom line: you __*must*__ handle rejected promises.
*
* # ManagedPromise/A+ Compatibility
*
* This `promise` module is compliant with the [ManagedPromise/A+][] specification
* except for sections `2.2.6.1` and `2.2.6.2`:
*
* >
* > - `then` may be called multiple times on the same promise.
* > - If/when `promise` is fulfilled, all respective `onFulfilled` callbacks
* > must execute in the order of their originating calls to `then`.
* > - If/when `promise` is rejected, all respective `onRejected` callbacks
* > must execute in the order of their originating calls to `then`.
* >
*
* Specifically, the conformance tests contains the following scenario (for
* brevity, only the fulfillment version is shown):
*
* var p1 = ManagedPromise.resolve();
* p1.then(function() {
* console.log('A');
* p1.then(() => console.log('B'));
* });
* p1.then(() => console.log('C'));
* // A
* // C
* // B
*
* Since the [ControlFlow](#scheduling_callbacks) executes promise callbacks as
* tasks, with this module, the result would be
*
* var p2 = promise.fulfilled();
* p2.then(function() {
* console.log('A');
* p2.then(() => console.log('B');
* });
* p2.then(() => console.log('C'));
* // A
* // B
* // C
*
* [JSEL]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
* [GF]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
* [ManagedPromise/A+]: https://promisesaplus.com/
*/
'use strict';
const error = require('./error');
const events = require('./events');
const logging = require('./logging');
/**
* Alias to help with readability and differentiate types.
* @const
*/
const NativePromise = Promise;
/**
* Whether to append traces of `then` to rejection errors.
* @type {boolean}
*/
var LONG_STACK_TRACES = false; // TODO: this should not be CONSTANT_CASE
/** @const */
const LOG = logging.getLogger('promise');
const UNIQUE_IDS = new WeakMap;
let nextId = 1;
function getUid(obj) {
let id = UNIQUE_IDS.get(obj);
if (!id) {
id = nextId;
nextId += 1;
UNIQUE_IDS.set(obj, id);
}
return id;
}
/**
* Runs the given function after a micro-task yield.
* @param {function()} fn The function to run.
*/
function asyncRun(fn) {
NativePromise.resolve().then(function() {
try {
fn();
} catch (ignored) {
// Do nothing.
}
});
}
/**
* @param {number} level What level of verbosity to log with.
* @param {(string|function(this: T): string)} loggable The message to log.
* @param {T=} opt_self The object in whose context to run the loggable
* function.
* @template T
*/
function vlog(level, loggable, opt_self) {
var logLevel = logging.Level.FINE;
if (level > 1) {
logLevel = logging.Level.FINEST;
} else if (level > 0) {
logLevel = logging.Level.FINER;
}
if (typeof loggable === 'function') {
loggable = loggable.bind(opt_self);
}
LOG.log(logLevel, loggable);
}
/**
* Generates an error to capture the current stack trace.
* @param {string} name Error name for this stack trace.
* @param {string} msg Message to record.
* @param {Function=} opt_topFn The function that should appear at the top of
* the stack; only applicable in V8.
* @return {!Error} The generated error.
*/
function captureStackTrace(name, msg, opt_topFn) {
var e = Error(msg);
e.name = name;
if (Error.captureStackTrace) {
Error.captureStackTrace(e, opt_topFn);
} else {
var stack = Error().stack;
if (stack) {
e.stack = e.toString();
e.stack += '\n' + stack;
}
}
return e;
}
/**
* Error used when the computation of a promise is cancelled.
*/
class CancellationError extends Error {
/**
* @param {string=} opt_msg The cancellation message.
*/
constructor(opt_msg) {
super(opt_msg);
/** @override */
this.name = this.constructor.name;
/** @private {boolean} */
this.silent_ = false;
}
/**
* Wraps the given error in a CancellationError.
*
* @param {*} error The error to wrap.
* @param {string=} opt_msg The prefix message to use.
* @return {!CancellationError} A cancellation error.
*/
static wrap(error, opt_msg) {
var message;
if (error instanceof CancellationError) {
return new CancellationError(
opt_msg ? (opt_msg + ': ' + error.message) : error.message);
} else if (opt_msg) {
message = opt_msg;
if (error) {
message += ': ' + error;
}
return new CancellationError(message);
}
if (error) {
message = error + '';
}
return new CancellationError(message);
}
}
/**
* Error used to cancel tasks when a control flow is reset.
* @final
*/
class FlowResetError extends CancellationError {
constructor() {
super('ControlFlow was reset');
this.silent_ = true;
}
}
/**
* Error used to cancel tasks that have been discarded due to an uncaught error
* reported earlier in the control flow.
* @final
*/
class DiscardedTaskError extends CancellationError {
/** @param {*} error The original error. */
constructor(error) {
if (error instanceof DiscardedTaskError) {
return /** @type {!DiscardedTaskError} */(error);
}
var msg = '';
if (error) {
msg = ': ' + (
typeof error.message === 'string' ? error.message : error);
}
super('Task was discarded due to a previous failure' + msg);
this.silent_ = true;
}
}
/**
* Error used when there are multiple unhandled promise rejections detected
* within a task or callback.
*
* @final
*/
class MultipleUnhandledRejectionError extends Error {
/**
* @param {!(Set<*>)} errors The errors to report.
*/
constructor(errors) {
super('Multiple unhandled promise rejections reported');
/** @override */
this.name = this.constructor.name;
/** @type {!Set<*>} */
this.errors = errors;
}
}
/**
* Property used to flag constructor's as implementing the Thenable interface
* for runtime type checking.
* @const
*/
const IMPLEMENTED_BY_SYMBOL = Symbol('promise.Thenable');
const CANCELLABLE_SYMBOL = Symbol('promise.CancellableThenable');
/**
* @param {function(new: ?)} ctor
* @param {!Object} symbol
*/
function addMarkerSymbol(ctor, symbol) {
try {
ctor.prototype[symbol] = true;
} catch (ignored) {
// Property access denied?
}
}
/**
* @param {*} object
* @param {!Object} symbol
* @return {boolean}
*/
function hasMarkerSymbol(object, symbol) {
if (!object) {
return false;
}
try {
return !!object[symbol];
} catch (e) {
return false; // Property access seems to be forbidden.
}
}
/**
* Thenable is a promise-like object with a {@code then} method which may be
* used to schedule callbacks on a promised value.
*
* @record
* @extends {IThenable}
* @template T
*/
class Thenable {
/**
* Adds a property to a class prototype to allow runtime checks of whether
* instances of that class implement the Thenable interface.
* @param {function(new: Thenable, ...?)} ctor The
* constructor whose prototype to modify.
*/
static addImplementation(ctor) {
addMarkerSymbol(ctor, IMPLEMENTED_BY_SYMBOL);
}
/**
* Checks if an object has been tagged for implementing the Thenable
* interface as defined by {@link Thenable.addImplementation}.
* @param {*} object The object to test.
* @return {boolean} Whether the object is an implementation of the Thenable
* interface.
*/
static isImplementation(object) {
return hasMarkerSymbol(object, IMPLEMENTED_BY_SYMBOL);
}
/**
* Registers listeners for when this instance is resolved.
*
* @param {?(function(T): (R|IThenable))=} opt_callback The
* function to call if this promise is successfully resolved. The function
* should expect a single argument: the promise's resolved value.
* @param {?(function(*): (R|IThenable))=} opt_errback
* The function to call if this promise is rejected. The function should
* expect a single argument: the rejection reason.
* @return {!Thenable} A new promise which will be resolved with the result
* of the invoked callback.
* @template R
*/
then(opt_callback, opt_errback) {}
/**
* Registers a listener for when this promise is rejected. This is synonymous
* with the {@code catch} clause in a synchronous API:
*
* // Synchronous API:
* try {
* doSynchronousWork();
* } catch (ex) {
* console.error(ex);
* }
*
* // Asynchronous promise API:
* doAsynchronousWork().catch(function(ex) {
* console.error(ex);
* });
*
* @param {function(*): (R|IThenable)} errback The
* function to call if this promise is rejected. The function should
* expect a single argument: the rejection reason.
* @return {!Thenable} A new promise which will be resolved with the result
* of the invoked callback.
* @template R
*/
catch(errback) {}
}
/**
* Marker interface for objects that allow consumers to request the cancellation
* of a promies-based operation. A cancelled promise will be rejected with a
* {@link CancellationError}.
*
* This interface is considered package-private and should not be used outside
* of selenium-webdriver.
*
* @interface
* @extends {Thenable}
* @template T
* @package
*/
class CancellableThenable {
/**
* @param {function(new: CancellableThenable, ...?)} ctor
*/
static addImplementation(ctor) {
Thenable.addImplementation(ctor);
addMarkerSymbol(ctor, CANCELLABLE_SYMBOL);
}
/**
* @param {*} object
* @return {boolean}
*/
static isImplementation(object) {
return hasMarkerSymbol(object, CANCELLABLE_SYMBOL);
}
/**
* Requests the cancellation of the computation of this promise's value,
* rejecting the promise in the process. This method is a no-op if the promise
* has already been resolved.
*
* @param {(string|Error)=} opt_reason The reason this promise is being
* cancelled. This value will be wrapped in a {@link CancellationError}.
*/
cancel(opt_reason) {}
}
/**
* @enum {string}
*/
const PromiseState = {
PENDING: 'pending',
BLOCKED: 'blocked',
REJECTED: 'rejected',
FULFILLED: 'fulfilled'
};
/**
* Internal map used to store cancellation handlers for {@link ManagedPromise}
* objects. This is an internal implementation detail used by the
* {@link TaskQueue} class to monitor for when a promise is cancelled without
* generating an extra promise via then().
*
* @const {!WeakMap}
*/
const ON_CANCEL_HANDLER = new WeakMap;
/**
* Represents the eventual value of a completed operation. Each promise may be
* in one of three states: pending, fulfilled, or rejected. Each promise starts
* in the pending state and may make a single transition to either a
* fulfilled or rejected state, at which point the promise is considered
* resolved.
*
* @implements {CancellableThenable}
* @template T
* @see http://promises-aplus.github.io/promises-spec/
*/
class ManagedPromise {
/**
* @param {function(
* function((T|IThenable|Thenable)=),
* function(*=))} resolver
* Function that is invoked immediately to begin computation of this
* promise's value. The function should accept a pair of callback
* functions, one for fulfilling the promise and another for rejecting it.
* @param {ControlFlow=} opt_flow The control flow
* this instance was created under. Defaults to the currently active flow.
*/
constructor(resolver, opt_flow) {
if (!usePromiseManager()) {
throw TypeError(
'Unable to create a managed promise instance: the promise manager has'
+ ' been disabled by the SELENIUM_PROMISE_MANAGER environment'
+ ' variable: ' + process.env['SELENIUM_PROMISE_MANAGER']);
}
getUid(this);
/** @private {!ControlFlow} */
this.flow_ = opt_flow || controlFlow();
/** @private {Error} */
this.stack_ = null;
if (LONG_STACK_TRACES) {
this.stack_ = captureStackTrace('ManagedPromise', 'new', this.constructor);
}
/** @private {Thenable>} */
this.parent_ = null;
/** @private {Array} */
this.callbacks_ = null;
/** @private {PromiseState} */
this.state_ = PromiseState.PENDING;
/** @private {boolean} */
this.handled_ = false;
/** @private {*} */
this.value_ = undefined;
/** @private {TaskQueue} */
this.queue_ = null;
try {
var self = this;
resolver(function(value) {
self.resolve_(PromiseState.FULFILLED, value);
}, function(reason) {
self.resolve_(PromiseState.REJECTED, reason);
});
} catch (ex) {
this.resolve_(PromiseState.REJECTED, ex);
}
}
/**
* Creates a promise that is immediately resolved with the given value.
*
* @param {T=} opt_value The value to resolve.
* @return {!ManagedPromise} A promise resolved with the given value.
* @template T
*/
static resolve(opt_value) {
if (opt_value instanceof ManagedPromise) {
return opt_value;
}
return new ManagedPromise(resolve => resolve(opt_value));
}
/**
* Creates a promise that is immediately rejected with the given reason.
*
* @param {*=} opt_reason The rejection reason.
* @return {!ManagedPromise>} A new rejected promise.
*/
static reject(opt_reason) {
return new ManagedPromise((_, reject) => reject(opt_reason));
}
/** @override */
toString() {
return 'ManagedPromise::' + getUid(this) +
' {[[PromiseStatus]]: "' + this.state_ + '"}';
}
/**
* Resolves this promise. If the new value is itself a promise, this function
* will wait for it to be resolved before notifying the registered listeners.
* @param {PromiseState} newState The promise's new state.
* @param {*} newValue The promise's new value.
* @throws {TypeError} If {@code newValue === this}.
* @private
*/
resolve_(newState, newValue) {
if (PromiseState.PENDING !== this.state_) {
return;
}
if (newValue === this) {
// See promise a+, 2.3.1
// http://promises-aplus.github.io/promises-spec/#point-48
newValue = new TypeError('A promise may not resolve to itself');
newState = PromiseState.REJECTED;
}
this.parent_ = null;
this.state_ = PromiseState.BLOCKED;
if (newState !== PromiseState.REJECTED) {
if (Thenable.isImplementation(newValue)) {
// 2.3.2
newValue = /** @type {!Thenable} */(newValue);
this.parent_ = newValue;
newValue.then(
this.unblockAndResolve_.bind(this, PromiseState.FULFILLED),
this.unblockAndResolve_.bind(this, PromiseState.REJECTED));
return;
} else if (newValue
&& (typeof newValue === 'object' || typeof newValue === 'function')) {
// 2.3.3
try {
// 2.3.3.1
var then = newValue['then'];
} catch (e) {
// 2.3.3.2
this.state_ = PromiseState.REJECTED;
this.value_ = e;
this.scheduleNotifications_();
return;
}
if (typeof then === 'function') {
// 2.3.3.3
this.invokeThen_(/** @type {!Object} */(newValue), then);
return;
}
}
}
if (newState === PromiseState.REJECTED &&
isError(newValue) && newValue.stack && this.stack_) {
newValue.stack += '\nFrom: ' + (this.stack_.stack || this.stack_);
}
// 2.3.3.4 and 2.3.4
this.state_ = newState;
this.value_ = newValue;
this.scheduleNotifications_();
}
/**
* Invokes a thenable's "then" method according to 2.3.3.3 of the promise
* A+ spec.
* @param {!Object} x The thenable object.
* @param {!Function} then The "then" function to invoke.
* @private
*/
invokeThen_(x, then) {
var called = false;
var self = this;
var resolvePromise = function(value) {
if (!called) { // 2.3.3.3.3
called = true;
// 2.3.3.3.1
self.unblockAndResolve_(PromiseState.FULFILLED, value);
}
};
var rejectPromise = function(reason) {
if (!called) { // 2.3.3.3.3
called = true;
// 2.3.3.3.2
self.unblockAndResolve_(PromiseState.REJECTED, reason);
}
};
try {
// 2.3.3.3
then.call(x, resolvePromise, rejectPromise);
} catch (e) {
// 2.3.3.3.4.2
rejectPromise(e);
}
}
/**
* @param {PromiseState} newState The promise's new state.
* @param {*} newValue The promise's new value.
* @private
*/
unblockAndResolve_(newState, newValue) {
if (this.state_ === PromiseState.BLOCKED) {
this.state_ = PromiseState.PENDING;
this.resolve_(newState, newValue);
}
}
/**
* @private
*/
scheduleNotifications_() {
vlog(2, () => this + ' scheduling notifications', this);
ON_CANCEL_HANDLER.delete(this);
if (this.value_ instanceof CancellationError
&& this.value_.silent_) {
this.callbacks_ = null;
}
if (!this.queue_) {
this.queue_ = this.flow_.getActiveQueue_();
}
if (!this.handled_ &&
this.state_ === PromiseState.REJECTED &&
!(this.value_ instanceof CancellationError)) {
this.queue_.addUnhandledRejection(this);
}
this.queue_.scheduleCallbacks(this);
}
/** @override */
cancel(opt_reason) {
if (!canCancel(this)) {
return;
}
if (this.parent_ && canCancel(this.parent_)) {
/** @type {!CancellableThenable} */(this.parent_).cancel(opt_reason);
} else {
var reason = CancellationError.wrap(opt_reason);
let onCancel = ON_CANCEL_HANDLER.get(this);
if (onCancel) {
onCancel(reason);
ON_CANCEL_HANDLER.delete(this);
}
if (this.state_ === PromiseState.BLOCKED) {
this.unblockAndResolve_(PromiseState.REJECTED, reason);
} else {
this.resolve_(PromiseState.REJECTED, reason);
}
}
function canCancel(promise) {
if (!(promise instanceof ManagedPromise)) {
return CancellableThenable.isImplementation(promise);
}
return promise.state_ === PromiseState.PENDING
|| promise.state_ === PromiseState.BLOCKED;
}
}
/** @override */
then(opt_callback, opt_errback) {
return this.addCallback_(
opt_callback, opt_errback, 'then', ManagedPromise.prototype.then);
}
/** @override */
catch(errback) {
return this.addCallback_(
null, errback, 'catch', ManagedPromise.prototype.catch);
}
/**
* @param {function(): (R|IThenable)} callback
* @return {!ManagedPromise}
* @template R
* @see ./promise.finally()
*/
finally(callback) {
let result = thenFinally(this, callback);
return /** @type {!ManagedPromise} */(result);
}
/**
* Registers a new callback with this promise
* @param {(function(T): (R|IThenable)|null|undefined)} callback The
* fulfillment callback.
* @param {(function(*): (R|IThenable)|null|undefined)} errback The
* rejection callback.
* @param {string} name The callback name.
* @param {!Function} fn The function to use as the top of the stack when
* recording the callback's creation point.
* @return {!ManagedPromise} A new promise which will be resolved with the
* esult of the invoked callback.
* @template R
* @private
*/
addCallback_(callback, errback, name, fn) {
if (typeof callback !== 'function' && typeof errback !== 'function') {
return this;
}
this.handled_ = true;
if (this.queue_) {
this.queue_.clearUnhandledRejection(this);
}
var cb = new Task(
this.flow_,
this.invokeCallback_.bind(this, callback, errback),
name,
LONG_STACK_TRACES ? {name: 'Promise', top: fn} : undefined);
cb.promise.parent_ = this;
if (this.state_ !== PromiseState.PENDING &&
this.state_ !== PromiseState.BLOCKED) {
this.flow_.getActiveQueue_().enqueue(cb);
} else {
if (!this.callbacks_) {
this.callbacks_ = [];
}
this.callbacks_.push(cb);
cb.blocked = true;
this.flow_.getActiveQueue_().enqueue(cb);
}
return cb.promise;
}
/**
* Invokes a callback function attached to this promise.
* @param {(function(T): (R|IThenable)|null|undefined)} callback The
* fulfillment callback.
* @param {(function(*): (R|IThenable)|null|undefined)} errback The
* rejection callback.
* @template R
* @private
*/
invokeCallback_(callback, errback) {
var callbackFn = callback;
if (this.state_ === PromiseState.REJECTED) {
callbackFn = errback;
}
if (typeof callbackFn === 'function') {
if (isGenerator(callbackFn)) {
return consume(callbackFn, null, this.value_);
}
return callbackFn(this.value_);
} else if (this.state_ === PromiseState.REJECTED) {
throw this.value_;
} else {
return this.value_;
}
}
}
CancellableThenable.addImplementation(ManagedPromise);
/**
* @param {!ManagedPromise} promise
* @return {boolean}
*/
function isPending(promise) {
return promise.state_ === PromiseState.PENDING;
}
/**
* Represents a value that will be resolved at some point in the future. This
* class represents the protected "producer" half of a ManagedPromise - each Deferred
* has a {@code promise} property that may be returned to consumers for
* registering callbacks, reserving the ability to resolve the deferred to the
* producer.
*
* If this Deferred is rejected and there are no listeners registered before
* the next turn of the event loop, the rejection will be passed to the
* {@link ControlFlow} as an unhandled failure.
*
* @template T
*/
class Deferred {
/**
* @param {ControlFlow=} opt_flow The control flow this instance was
* created under. This should only be provided during unit tests.
*/
constructor(opt_flow) {
var fulfill, reject;
/** @type {!ManagedPromise} */
this.promise = new ManagedPromise(function(f, r) {
fulfill = f;
reject = r;
}, opt_flow);
var self = this;
var checkNotSelf = function(value) {
if (value === self) {
throw new TypeError('May not resolve a Deferred with itself');
}
};
/**
* Resolves this deferred with the given value. It is safe to call this as a
* normal function (with no bound "this").
* @param {(T|IThenable|Thenable)=} opt_value The fulfilled value.
*/
this.fulfill = function(opt_value) {
checkNotSelf(opt_value);
fulfill(opt_value);
};
/**
* Rejects this promise with the given reason. It is safe to call this as a
* normal function (with no bound "this").
* @param {*=} opt_reason The rejection reason.
*/
this.reject = function(opt_reason) {
checkNotSelf(opt_reason);
reject(opt_reason);
};
}
}
/**
* Tests if a value is an Error-like object. This is more than an straight
* instanceof check since the value may originate from another context.
* @param {*} value The value to test.
* @return {boolean} Whether the value is an error.
*/
function isError(value) {
return value instanceof Error ||
(!!value && typeof value === 'object'
&& typeof value.message === 'string');
}
/**
* Determines whether a {@code value} should be treated as a promise.
* Any object whose "then" property is a function will be considered a promise.
*
* @param {?} value The value to test.
* @return {boolean} Whether the value is a promise.
*/
function isPromise(value) {
try {
// Use array notation so the Closure compiler does not obfuscate away our
// contract.
return value
&& (typeof value === 'object' || typeof value === 'function')
&& typeof value['then'] === 'function';
} catch (ex) {
return false;
}
}
/**
* Creates a promise that will be resolved at a set time in the future.
* @param {number} ms The amount of time, in milliseconds, to wait before
* resolving the promise.
* @return {!Thenable} The promise.
*/
function delayed(ms) {
return createPromise(resolve => {
setTimeout(() => resolve(), ms);
});
}
/**
* Creates a new deferred object.
* @return {!Deferred} The new deferred object.
* @template T
*/
function defer() {
return new Deferred();
}
/**
* Creates a promise that has been resolved with the given value.
* @param {T=} opt_value The resolved value.
* @return {!ManagedPromise} The resolved promise.
* @deprecated Use {@link ManagedPromise#resolve Promise.resolve(value)}.
* @template T
*/
function fulfilled(opt_value) {
return ManagedPromise.resolve(opt_value);
}
/**
* Creates a promise that has been rejected with the given reason.
* @param {*=} opt_reason The rejection reason; may be any value, but is
* usually an Error or a string.
* @return {!ManagedPromise>} The rejected promise.
* @deprecated Use {@link ManagedPromise#reject Promise.reject(reason)}.
*/
function rejected(opt_reason) {
return ManagedPromise.reject(opt_reason);
}
/**
* Wraps a function that expects a node-style callback as its final
* argument. This callback expects two arguments: an error value (which will be
* null if the call succeeded), and the success value as the second argument.
* The callback will the resolve or reject the returned promise, based on its
* arguments.
* @param {!Function} fn The function to wrap.
* @param {...?} var_args The arguments to apply to the function, excluding the
* final callback.
* @return {!Thenable} A promise that will be resolved with the
* result of the provided function's callback.
*/
function checkedNodeCall(fn, var_args) {
let args = Array.prototype.slice.call(arguments, 1);
return createPromise(function(fulfill, reject) {
try {
args.push(function(error, value) {
error ? reject(error) : fulfill(value);
});
fn.apply(undefined, args);
} catch (ex) {
reject(ex);
}
});
}
/**
* Registers a listener to invoke when a promise is resolved, regardless
* of whether the promise's value was successfully computed. This function
* is synonymous with the {@code finally} clause in a synchronous API:
*
* // Synchronous API:
* try {
* doSynchronousWork();
* } finally {
* cleanUp();
* }
*
* // Asynchronous promise API:
* doAsynchronousWork().finally(cleanUp);
*
* __Note:__ similar to the {@code finally} clause, if the registered
* callback returns a rejected promise or throws an error, it will silently
* replace the rejection error (if any) from this promise:
*
* try {
* throw Error('one');
* } finally {
* throw Error('two'); // Hides Error: one
* }
*
* let p = Promise.reject(Error('one'));
* promise.finally(p, function() {
* throw Error('two'); // Hides Error: one
* });
*
* @param {!IThenable>} promise The promise to add the listener to.
* @param {function(): (R|IThenable)} callback The function to call when
* the promise is resolved.
* @return {!IThenable} A promise that will be resolved with the callback
* result.
* @template R
*/
function thenFinally(promise, callback) {
let error;
let mustThrow = false;
return promise.then(function() {
return callback();
}, function(err) {
error = err;
mustThrow = true;
return callback();
}).then(function() {
if (mustThrow) {
throw error;
}
});
}
/**
* Registers an observer on a promised {@code value}, returning a new promise
* that will be resolved when the value is. If {@code value} is not a promise,
* then the return promise will be immediately resolved.
* @param {*} value The value to observe.
* @param {Function=} opt_callback The function to call when the value is
* resolved successfully.
* @param {Function=} opt_errback The function to call when the value is
* rejected.
* @return {!Thenable} A new promise.
*/
function when(value, opt_callback, opt_errback) {
if (Thenable.isImplementation(value)) {
return value.then(opt_callback, opt_errback);
}
return createPromise(resolve => resolve(value))
.then(opt_callback, opt_errback);
}
/**
* Invokes the appropriate callback function as soon as a promised `value` is
* resolved. This function is similar to `when()`, except it does not return
* a new promise.
* @param {*} value The value to observe.
* @param {Function} callback The function to call when the value is
* resolved successfully.
* @param {Function=} opt_errback The function to call when the value is
* rejected.
*/
function asap(value, callback, opt_errback) {
if (isPromise(value)) {
value.then(callback, opt_errback);
} else if (callback) {
callback(value);
}
}
/**
* Given an array of promises, will return a promise that will be fulfilled
* with the fulfillment values of the input array's values. If any of the
* input array's promises are rejected, the returned promise will be rejected
* with the same reason.
*
* @param {!Array<(T|!ManagedPromise)>} arr An array of
* promises to wait on.
* @return {!Thenable>} A promise that is
* fulfilled with an array containing the fulfilled values of the
* input array, or rejected with the same reason as the first
* rejected value.
* @template T
*/
function all(arr) {
return createPromise(function(fulfill, reject) {
var n = arr.length;
var values = [];
if (!n) {
fulfill(values);
return;
}
var toFulfill = n;
var onFulfilled = function(index, value) {
values[index] = value;
toFulfill--;
if (toFulfill == 0) {
fulfill(values);
}
};
function processPromise(index) {
asap(arr[index], function(value) {
onFulfilled(index, value);
}, reject);
}
for (var i = 0; i < n; ++i) {
processPromise(i);
}
});
}
/**
* Calls a function for each element in an array and inserts the result into a
* new array, which is used as the fulfillment value of the promise returned
* by this function.
*
* If the return value of the mapping function is a promise, this function
* will wait for it to be fulfilled before inserting it into the new array.
*
* If the mapping function throws or returns a rejected promise, the
* promise returned by this function will be rejected with the same reason.
* Only the first failure will be reported; all subsequent errors will be
* silently ignored.
*
* @param {!(Array|ManagedPromise>)} arr The
* array to iterator over, or a promise that will resolve to said array.
* @param {function(this: SELF, TYPE, number, !Array): ?} fn The
* function to call for each element in the array. This function should
* expect three arguments (the element, the index, and the array itself.
* @param {SELF=} opt_self The object to be used as the value of 'this' within
* {@code fn}.
* @template TYPE, SELF
*/
function map(arr, fn, opt_self) {
return createPromise(resolve => resolve(arr)).then(v => {
if (!Array.isArray(v)) {
throw TypeError('not an array');
}
var arr = /** @type {!Array} */(v);
return createPromise(function(fulfill, reject) {
var n = arr.length;
var values = new Array(n);
(function processNext(i) {
for (; i < n; i++) {
if (i in arr) {
break;
}
}
if (i >= n) {
fulfill(values);
return;
}
try {
asap(
fn.call(opt_self, arr[i], i, /** @type {!Array} */(arr)),
function(value) {
values[i] = value;
processNext(i + 1);
},
reject);
} catch (ex) {
reject(ex);
}
})(0);
});
});
}
/**
* Calls a function for each element in an array, and if the function returns
* true adds the element to a new array.
*
* If the return value of the filter function is a promise, this function
* will wait for it to be fulfilled before determining whether to insert the
* element into the new array.
*
* If the filter function throws or returns a rejected promise, the promise
* returned by this function will be rejected with the same reason. Only the
* first failure will be reported; all subsequent errors will be silently
* ignored.
*
* @param {!(Array|ManagedPromise>)} arr The
* array to iterator over, or a promise that will resolve to said array.
* @param {function(this: SELF, TYPE, number, !Array): (
* boolean|ManagedPromise)} fn The function
* to call for each element in the array.
* @param {SELF=} opt_self The object to be used as the value of 'this' within
* {@code fn}.
* @template TYPE, SELF
*/
function filter(arr, fn, opt_self) {
return createPromise(resolve => resolve(arr)).then(v => {
if (!Array.isArray(v)) {
throw TypeError('not an array');
}
var arr = /** @type {!Array} */(v);
return createPromise(function(fulfill, reject) {
var n = arr.length;
var values = [];
var valuesLength = 0;
(function processNext(i) {
for (; i < n; i++) {
if (i in arr) {
break;
}
}
if (i >= n) {
fulfill(values);
return;
}
try {
var value = arr[i];
var include = fn.call(opt_self, value, i, /** @type {!Array} */(arr));
asap(include, function(include) {
if (include) {
values[valuesLength++] = value;
}
processNext(i + 1);
}, reject);
} catch (ex) {
reject(ex);
}
})(0);
});
});
}
/**
* Returns a promise that will be resolved with the input value in a
* fully-resolved state. If the value is an array, each element will be fully
* resolved. Likewise, if the value is an object, all keys will be fully
* resolved. In both cases, all nested arrays and objects will also be
* fully resolved. All fields are resolved in place; the returned promise will
* resolve on {@code value} and not a copy.
*
* Warning: This function makes no checks against objects that contain
* cyclical references:
*
* var value = {};
* value['self'] = value;
* promise.fullyResolved(value); // Stack overflow.
*
* @param {*} value The value to fully resolve.
* @return {!Thenable} A promise for a fully resolved version
* of the input value.
*/
function fullyResolved(value) {
if (isPromise(value)) {
return when(value, fullyResolveValue);
}
return fullyResolveValue(value);
}
/**
* @param {*} value The value to fully resolve. If a promise, assumed to
* already be resolved.
* @return {!Thenable} A promise for a fully resolved version
* of the input value.
*/
function fullyResolveValue(value) {
if (Array.isArray(value)) {
return fullyResolveKeys(/** @type {!Array} */ (value));
}
if (isPromise(value)) {
if (isPromise(value)) {
// We get here when the original input value is a promise that
// resolves to itself. When the user provides us with such a promise,
// trust that it counts as a "fully resolved" value and return it.
// Of course, since it's already a promise, we can just return it
// to the user instead of wrapping it in another promise.
return /** @type {!ManagedPromise} */ (value);
}
}
if (value && typeof value === 'object') {
return fullyResolveKeys(/** @type {!Object} */ (value));
}
if (typeof value === 'function') {
return fullyResolveKeys(/** @type {!Object} */ (value));
}
return createPromise(resolve => resolve(value));
}
/**
* @param {!(Array|Object)} obj the object to resolve.
* @return {!Thenable} A promise that will be resolved with the
* input object once all of its values have been fully resolved.
*/
function fullyResolveKeys(obj) {
var isArray = Array.isArray(obj);
var numKeys = isArray ? obj.length : (function() {
let n = 0;
for (let key in obj) {
n += 1;
}
return n;
})();
if (!numKeys) {
return createPromise(resolve => resolve(obj));
}
function forEachProperty(obj, fn) {
for (let key in obj) {
fn.call(null, obj[key], key, obj);
}
}
function forEachElement(arr, fn) {
arr.forEach(fn);
}
var numResolved = 0;
return createPromise(function(fulfill, reject) {
var forEachKey = isArray ? forEachElement: forEachProperty;
forEachKey(obj, function(partialValue, key) {
if (!Array.isArray(partialValue)
&& (!partialValue || typeof partialValue !== 'object')) {
maybeResolveValue();
return;
}
fullyResolved(partialValue).then(
function(resolvedValue) {
obj[key] = resolvedValue;
maybeResolveValue();
},
reject);
});
function maybeResolveValue() {
if (++numResolved == numKeys) {
fulfill(obj);
}
}
});
}
//////////////////////////////////////////////////////////////////////////////
//
// ControlFlow
//
//////////////////////////////////////////////////////////////////////////////
/**
* Defines methods for coordinating the execution of asynchronous tasks.
* @record
*/
class Scheduler {
/**
* Schedules a task for execution. If the task function is a generator, the
* task will be executed using {@link ./promise.consume consume()}.
*
* @param {function(): (T|IThenable)} fn The function to call to start the
* task.
* @param {string=} opt_description A description of the task for debugging
* purposes.
* @return {!Thenable} A promise that will be resolved with the task
* result.
* @template T
*/
execute(fn, opt_description) {}
/**
* Creates a new promise using the given resolver function.
*
* @param {function(
* function((T|IThenable|Thenable|null)=),
* function(*=))} resolver
* @return {!Thenable}
* @template T
*/
promise(resolver) {}
/**
* Schedules a `setTimeout` call.
*
* @param {number} ms The timeout delay, in milliseconds.
* @param {string=} opt_description A description to accompany the timeout.
* @return {!Thenable} A promise that will be resolved when the timeout
* fires.
*/
timeout(ms, opt_description) {}
/**
* Schedules a task to wait for a condition to hold.
*
* If the condition is defined as a function, it may return any value. Promies
* will be resolved before testing if the condition holds (resolution time
* counts towards the timeout). Once resolved, values are always evaluated as
* booleans.
*
* If the condition function throws, or returns a rejected promise, the
* wait task will fail.
*
* If the condition is defined as a promise, the scheduler will wait for it to
* settle. If the timeout expires before the promise settles, the promise
* returned by this function will be rejected.
*
* If this function is invoked with `timeout === 0`, or the timeout is
* omitted, this scheduler will wait indefinitely for the condition to be
* satisfied.
*
* @param {(!IThenable|function())} condition The condition to poll,
* or a promise to wait on.
* @param {number=} opt_timeout How long to wait, in milliseconds, for the
* condition to hold before timing out. If omitted, the flow will wait
* indefinitely.
* @param {string=} opt_message An optional error message to include if the
* wait times out; defaults to the empty string.
* @return {!Thenable} A promise that will be fulfilled
* when the condition has been satisified. The promise shall be rejected
* if the wait times out waiting for the condition.
* @throws {TypeError} If condition is not a function or promise or if timeout
* is not a number >= 0.
* @template T
*/
wait(condition, opt_timeout, opt_message) {}
}
let USE_PROMISE_MANAGER;
function usePromiseManager() {
if (typeof USE_PROMISE_MANAGER !== 'undefined') {
return !!USE_PROMISE_MANAGER;
}
return process.env['SELENIUM_PROMISE_MANAGER'] === undefined
|| !/^0|false$/i.test(process.env['SELENIUM_PROMISE_MANAGER']);
}
/**
* @param {function(
* function((T|IThenable|Thenable|null)=),
* function(*=))} resolver
* @return {!Thenable}
* @template T
*/
function createPromise(resolver) {
let ctor = usePromiseManager() ? ManagedPromise : NativePromise;
return new ctor(resolver);
}
/**
* @param {!Scheduler} scheduler The scheduler to use.
* @param {(!IThenable|function())} condition The condition to poll,
* or a promise to wait on.
* @param {number=} opt_timeout How long to wait, in milliseconds, for the
* condition to hold before timing out. If omitted, the flow will wait
* indefinitely.
* @param {string=} opt_message An optional error message to include if the
* wait times out; defaults to the empty string.
* @return {!Thenable} A promise that will be fulfilled
* when the condition has been satisified. The promise shall be rejected
* if the wait times out waiting for the condition.
* @throws {TypeError} If condition is not a function or promise or if timeout
* is not a number >= 0.
* @template T
*/
function scheduleWait(scheduler, condition, opt_timeout, opt_message) {
let timeout = opt_timeout || 0;
if (typeof timeout !== 'number' || timeout < 0) {
throw TypeError('timeout must be a number >= 0: ' + timeout);
}
if (isPromise(condition)) {
return scheduler.execute(function() {
if (!timeout) {
return condition;
}
return scheduler.promise(function(fulfill, reject) {
let start = Date.now();
let timer = setTimeout(function() {
timer = null;
reject(
new error.TimeoutError(
(opt_message ? opt_message + '\n' : '')
+ 'Timed out waiting for promise to resolve after '
+ (Date.now() - start) + 'ms'));
}, timeout);
/** @type {Thenable} */(condition).then(
function(value) {
timer && clearTimeout(timer);
fulfill(value);
},
function(error) {
timer && clearTimeout(timer);
reject(error);
});
});
}, opt_message || '');
}
if (typeof condition !== 'function') {
throw TypeError('Invalid condition; must be a function or promise: ' +
typeof condition);
}
if (isGenerator(condition)) {
let original = condition;
condition = () => consume(original);
}
return scheduler.execute(function() {
var startTime = Date.now();
return scheduler.promise(function(fulfill, reject) {
pollCondition();
function pollCondition() {
var conditionFn = /** @type {function()} */(condition);
scheduler.execute(conditionFn).then(function(value) {
var elapsed = Date.now() - startTime;
if (!!value) {
fulfill(value);
} else if (timeout && elapsed >= timeout) {
reject(
new error.TimeoutError(
(opt_message ? opt_message + '\n' : '')
+ `Wait timed out after ${elapsed}ms`));
} else {
// Do not use asyncRun here because we need a non-micro yield
// here so the UI thread is given a chance when running in a
// browser.
setTimeout(pollCondition, 0);
}
}, reject);
}
});
}, opt_message || '');
}
/**
* A scheduler that executes all tasks immediately, with no coordination. This
* class is an event emitter for API compatibility with the {@link ControlFlow},
* however, it emits no events.
*
* @implements {Scheduler}
*/
class SimpleScheduler extends events.EventEmitter {
/** @override */
execute(fn) {
return this.promise((resolve, reject) => {
try {
if (isGenerator(fn)) {
consume(fn).then(resolve, reject);
} else {
resolve(fn.call(undefined));
}
} catch (ex) {
reject(ex);
}
});
}
/** @override */
promise(resolver) {
return new NativePromise(resolver);
}
/** @override */
timeout(ms) {
return this.promise(resolve => setTimeout(_ => resolve(), ms));
}
/** @override */
wait(condition, opt_timeout, opt_message) {
return scheduleWait(this, condition, opt_timeout, opt_message);
}
}
const SIMPLE_SCHEDULER = new SimpleScheduler;
/**
* Handles the execution of scheduled tasks, each of which may be an
* asynchronous operation. The control flow will ensure tasks are executed in
* the ordered scheduled, starting each task only once those before it have
* completed.
*
* Each task scheduled within this flow may return a {@link ManagedPromise} to
* indicate it is an asynchronous operation. The ControlFlow will wait for such
* promises to be resolved before marking the task as completed.
*
* Tasks and each callback registered on a {@link ManagedPromise} will be run
* in their own ControlFlow frame. Any tasks scheduled within a frame will take
* priority over previously scheduled tasks. Furthermore, if any of the tasks in
* the frame fail, the remainder of the tasks in that frame will be discarded
* and the failure will be propagated to the user through the callback/task's
* promised result.
*
* Each time a ControlFlow empties its task queue, it will fire an
* {@link ControlFlow.EventType.IDLE IDLE} event. Conversely,
* whenever the flow terminates due to an unhandled error, it will remove all
* remaining tasks in its queue and fire an
* {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION UNCAUGHT_EXCEPTION} event.
* If there are no listeners registered with the flow, the error will be
* rethrown to the global error handler.
*
* Refer to the {@link ./promise} module documentation for a detailed
* explanation of how the ControlFlow coordinates task execution.
*
* @implements {Scheduler}
* @final
*/
class ControlFlow extends events.EventEmitter {
constructor() {
if (!usePromiseManager()) {
throw TypeError(
'Cannot instantiate control flow when the promise manager has'
+ ' been disabled');
}
super();
/** @private {boolean} */
this.propagateUnhandledRejections_ = true;
/** @private {TaskQueue} */
this.activeQueue_ = null;
/** @private {Set} */
this.taskQueues_ = null;
/**
* Micro task that controls shutting down the control flow. Upon shut down,
* the flow will emit an
* {@link ControlFlow.EventType.IDLE} event. Idle events
* always follow a brief timeout in order to catch latent errors from the
* last completed task. If this task had a callback registered, but no
* errback, and the task fails, the unhandled failure would not be reported
* by the promise system until the next turn of the event loop:
*
* // Schedule 1 task that fails.
* var result = promise.controlFlow().schedule('example',
* function() { return promise.rejected('failed'); });
* // Set a callback on the result. This delays reporting the unhandled
* // failure for 1 turn of the event loop.
* result.then(function() {});
*
* @private {MicroTask}
*/
this.shutdownTask_ = null;
/**
* ID for a long running interval used to keep a Node.js process running
* while a control flow's event loop is still working. This is a cheap hack
* required since JS events are only scheduled to run when there is
* _actually_ something to run. When a control flow is waiting on a task,
* there will be nothing in the JS event loop and the process would
* terminate without this.
* @private
*/
this.hold_ = null;
}
/**
* Returns a string representation of this control flow, which is its current
* {@linkplain #getSchedule() schedule}, sans task stack traces.
* @return {string} The string representation of this contorl flow.
* @override
*/
toString() {
return this.getSchedule();
}
/**
* Sets whether any unhandled rejections should propagate up through the
* control flow stack and cause rejections within parent tasks. If error
* propagation is disabled, tasks will not be aborted when an unhandled
* promise rejection is detected, but the rejection _will_ trigger an
* {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION}
* event.
*
* The default behavior is to propagate all unhandled rejections. _The use
* of this option is highly discouraged._
*
* @param {boolean} propagate whether to propagate errors.
*/
setPropagateUnhandledRejections(propagate) {
this.propagateUnhandledRejections_ = propagate;
}
/**
* @return {boolean} Whether this flow is currently idle.
*/
isIdle() {
return !this.shutdownTask_ && (!this.taskQueues_ || !this.taskQueues_.size);
}
/**
* Resets this instance, clearing its queue and removing all event listeners.
*/
reset() {
this.cancelQueues_(new FlowResetError);
this.emit(ControlFlow.EventType.RESET);
this.removeAllListeners();
this.cancelShutdown_();
}
/**
* Generates an annotated string describing the internal state of this control
* flow, including the currently executing as well as pending tasks. If
* {@code opt_includeStackTraces === true}, the string will include the
* stack trace from when each task was scheduled.
* @param {string=} opt_includeStackTraces Whether to include the stack traces
* from when each task was scheduled. Defaults to false.
* @return {string} String representation of this flow's internal state.
*/
getSchedule(opt_includeStackTraces) {
var ret = 'ControlFlow::' + getUid(this);
var activeQueue = this.activeQueue_;
if (!this.taskQueues_ || !this.taskQueues_.size) {
return ret;
}
var childIndent = '| ';
for (var q of this.taskQueues_) {
ret += '\n' + printQ(q, childIndent);
}
return ret;
function printQ(q, indent) {
var ret = q.toString();
if (q === activeQueue) {
ret = '(active) ' + ret;
}
var prefix = indent + childIndent;
if (q.pending_) {
if (q.pending_.q.state_ !== TaskQueueState.FINISHED) {
ret += '\n' + prefix + '(pending) ' + q.pending_.task;
ret += '\n' + printQ(q.pending_.q, prefix + childIndent);
} else {
ret += '\n' + prefix + '(blocked) ' + q.pending_.task;
}
}
if (q.interrupts_) {
q.interrupts_.forEach((task) => {
ret += '\n' + prefix + task;
});
}
if (q.tasks_) {
q.tasks_.forEach((task) => ret += printTask(task, '\n' + prefix));
}
return indent + ret;
}
function printTask(task, prefix) {
var ret = prefix + task;
if (opt_includeStackTraces && task.promise.stack_) {
ret += prefix + childIndent
+ (task.promise.stack_.stack || task.promise.stack_)
.replace(/\n/g, prefix);
}
return ret;
}
}
/**
* Returns the currently actively task queue for this flow. If there is no
* active queue, one will be created.
* @return {!TaskQueue} the currently active task queue for this flow.
* @private
*/
getActiveQueue_() {
if (this.activeQueue_) {
return this.activeQueue_;
}
this.activeQueue_ = new TaskQueue(this);
if (!this.taskQueues_) {
this.taskQueues_ = new Set();
}
this.taskQueues_.add(this.activeQueue_);
this.activeQueue_
.once('end', this.onQueueEnd_, this)
.once('error', this.onQueueError_, this);
asyncRun(() => this.activeQueue_ = null);
this.activeQueue_.start();
return this.activeQueue_;
}
/** @override */
execute(fn, opt_description) {
if (isGenerator(fn)) {
let original = fn;
fn = () => consume(original);
}
if (!this.hold_) {
var holdIntervalMs = 2147483647; // 2^31-1; max timer length for Node.js
this.hold_ = setInterval(function() {}, holdIntervalMs);
}
var task = new Task(
this, fn, opt_description || '',
{name: 'Task', top: ControlFlow.prototype.execute});
var q = this.getActiveQueue_();
q.enqueue(task);
this.emit(ControlFlow.EventType.SCHEDULE_TASK, task.description);
return task.promise;
}
/** @override */
promise(resolver) {
return new ManagedPromise(resolver, this);
}
/** @override */
timeout(ms, opt_description) {
return this.execute(() => {
return this.promise(resolve => setTimeout(() => resolve(), ms));
}, opt_description);
}
/** @override */
wait(condition, opt_timeout, opt_message) {
return scheduleWait(this, condition, opt_timeout, opt_message);
}
/**
* Executes a function in the next available turn of the JavaScript event
* loop. This ensures the function runs with its own task queue and any
* scheduled tasks will run in "parallel" to those scheduled in the current
* function.
*
* flow.execute(() => console.log('a'));
* flow.execute(() => console.log('b'));
* flow.execute(() => console.log('c'));
* flow.async(() => {
* flow.execute(() => console.log('d'));
* flow.execute(() => console.log('e'));
* });
* flow.async(() => {
* flow.execute(() => console.log('f'));
* flow.execute(() => console.log('g'));
* });
* flow.once('idle', () => console.log('fin'));
* // a
* // d
* // f
* // b
* // e
* // g
* // c
* // fin
*
* If the function itself throws, the error will be treated the same as an
* unhandled rejection within the control flow.
*
* __NOTE__: This function is considered _unstable_.
*
* @param {!Function} fn The function to execute.
* @param {Object=} opt_self The object in whose context to run the function.
* @param {...*} var_args Any arguments to pass to the function.
*/
async(fn, opt_self, var_args) {
asyncRun(() => {
// Clear any lingering queues, forces getActiveQueue_ to create a new one.
this.activeQueue_ = null;
var q = this.getActiveQueue_();
try {
q.execute_(fn.bind(opt_self, var_args));
} catch (ex) {
var cancellationError = CancellationError.wrap(ex,
'Function passed to ControlFlow.async() threw');
cancellationError.silent_ = true;
q.abort_(cancellationError);
} finally {
this.activeQueue_ = null;
}
});
}
/**
* Event handler for when a task queue is exhausted. This starts the shutdown
* sequence for this instance if there are no remaining task queues: after
* one turn of the event loop, this object will emit the
* {@link ControlFlow.EventType.IDLE IDLE} event to signal
* listeners that it has completed. During this wait, if another task is
* scheduled, the shutdown will be aborted.
*
* @param {!TaskQueue} q the completed task queue.
* @private
*/
onQueueEnd_(q) {
if (!this.taskQueues_) {
return;
}
this.taskQueues_.delete(q);
vlog(1, () => q + ' has finished');
vlog(1, () => this.taskQueues_.size + ' queues remain\n' + this, this);
if (!this.taskQueues_.size) {
if (this.shutdownTask_) {
throw Error('Already have a shutdown task??');
}
vlog(1, () => 'Scheduling shutdown\n' + this);
this.shutdownTask_ = new MicroTask(() => this.shutdown_());
}
}
/**
* Event handler for when a task queue terminates with an error. This triggers
* the cancellation of all other task queues and a
* {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION} event.
* If there are no error event listeners registered with this instance, the
* error will be rethrown to the global error handler.
*
* @param {*} error the error that caused the task queue to terminate.
* @param {!TaskQueue} q the task queue.
* @private
*/
onQueueError_(error, q) {
if (this.taskQueues_) {
this.taskQueues_.delete(q);
}
this.cancelQueues_(CancellationError.wrap(
error, 'There was an uncaught error in the control flow'));
this.cancelShutdown_();
this.cancelHold_();
setTimeout(() => {
let listeners = this.listeners(ControlFlow.EventType.UNCAUGHT_EXCEPTION);
if (!listeners.size) {
throw error;
} else {
this.reportUncaughtException_(error);
}
}, 0);
}
/**
* Cancels all remaining task queues.
* @param {!CancellationError} reason The cancellation reason.
* @private
*/
cancelQueues_(reason) {
reason.silent_ = true;
if (this.taskQueues_) {
for (var q of this.taskQueues_) {
q.removeAllListeners();
q.abort_(reason);
}
this.taskQueues_.clear();
this.taskQueues_ = null;
}
}
/**
* Reports an uncaught exception using a
* {@link ControlFlow.EventType.UNCAUGHT_EXCEPTION} event.
*
* @param {*} e the error to report.
* @private
*/
reportUncaughtException_(e) {
this.emit(ControlFlow.EventType.UNCAUGHT_EXCEPTION, e);
}
/** @private */
cancelHold_() {
if (this.hold_) {
clearInterval(this.hold_);
this.hold_ = null;
}
}
/** @private */
shutdown_() {
vlog(1, () => 'Going idle: ' + this);
this.cancelHold_();
this.shutdownTask_ = null;
this.emit(ControlFlow.EventType.IDLE);
}
/**
* Cancels the shutdown sequence if it is currently scheduled.
* @private
*/
cancelShutdown_() {
if (this.shutdownTask_) {
this.shutdownTask_.cancel();
this.shutdownTask_ = null;
}
}
}
/**
* Events that may be emitted by an {@link ControlFlow}.
* @enum {string}
*/
ControlFlow.EventType = {
/** Emitted when all tasks have been successfully executed. */
IDLE: 'idle',
/** Emitted when a ControlFlow has been reset. */
RESET: 'reset',
/** Emitted whenever a new task has been scheduled. */
SCHEDULE_TASK: 'scheduleTask',
/**
* Emitted whenever a control flow aborts due to an unhandled promise
* rejection. This event will be emitted along with the offending rejection
* reason. Upon emitting this event, the control flow will empty its task
* queue and revert to its initial state.
*/
UNCAUGHT_EXCEPTION: 'uncaughtException'
};
/**
* Wraps a function to execute as a cancellable micro task.
* @final
*/
class MicroTask {
/**
* @param {function()} fn The function to run as a micro task.
*/
constructor(fn) {
/** @private {boolean} */
this.cancelled_ = false;
asyncRun(() => {
if (!this.cancelled_) {
fn();
}
});
}
/**
* Runs the given function after a micro-task yield.
* @param {function()} fn The function to run.
*/
static run(fn) {
NativePromise.resolve().then(function() {
try {
fn();
} catch (ignored) {
// Do nothing.
}
});
}
/**
* Cancels the execution of this task. Note: this will not prevent the task
* timer from firing, just the invocation of the wrapped function.
*/
cancel() {
this.cancelled_ = true;
}
}
/**
* A task to be executed by a {@link ControlFlow}.
*
* @template T
* @final
*/
class Task extends Deferred {
/**
* @param {!ControlFlow} flow The flow this instances belongs
* to.
* @param {function(): (T|!ManagedPromise)} fn The function to
* call when the task executes. If it returns a
* {@link ManagedPromise}, the flow will wait for it to be
* resolved before starting the next task.
* @param {string} description A description of the task for debugging.
* @param {{name: string, top: !Function}=} opt_stackOptions Options to use
* when capturing the stacktrace for when this task was created.
*/
constructor(flow, fn, description, opt_stackOptions) {
super(flow);
getUid(this);
/** @type {function(): (T|!ManagedPromise)} */
this.execute = fn;
/** @type {string} */
this.description = description;
/** @type {TaskQueue} */
this.queue = null;
/**
* Whether this task is considered block. A blocked task may be registered
* in a task queue, but will be dropped if it is still blocked when it
* reaches the front of the queue. A dropped task may always be rescheduled.
*
* Blocked tasks are used when a callback is attached to an unsettled
* promise to reserve a spot in line (in a manner of speaking). If the
* promise is not settled before the callback reaches the front of the
* of the queue, it will be dropped. Once the promise is settled, the
* dropped task will be rescheduled as an interrupt on the currently task
* queue.
*
* @type {boolean}
*/
this.blocked = false;
if (opt_stackOptions) {
this.promise.stack_ = captureStackTrace(
opt_stackOptions.name, this.description, opt_stackOptions.top);
}
}
/** @override */
toString() {
return 'Task::' + getUid(this) + '<' + this.description + '>';
}
}
/** @enum {string} */
const TaskQueueState = {
NEW: 'new',
STARTED: 'started',
FINISHED: 'finished'
};
/**
* @final
*/
class TaskQueue extends events.EventEmitter {
/** @param {!ControlFlow} flow . */
constructor(flow) {
super();
/** @private {string} */
this.name_ = 'TaskQueue::' + getUid(this);
/** @private {!ControlFlow} */
this.flow_ = flow;
/** @private {!Array} */
this.tasks_ = [];
/** @private {Array} */
this.interrupts_ = null;
/** @private {({task: !Task, q: !TaskQueue}|null)} */
this.pending_ = null;
/** @private {TaskQueue} */
this.subQ_ = null;
/** @private {TaskQueueState} */
this.state_ = TaskQueueState.NEW;
/** @private {!Set} */
this.unhandledRejections_ = new Set();
}
/** @override */
toString() {
return 'TaskQueue::' + getUid(this);
}
/**
* @param {!ManagedPromise} promise .
*/
addUnhandledRejection(promise) {
// TODO: node 4.0.0+
vlog(2, () => this + ' registering unhandled rejection: ' + promise, this);
this.unhandledRejections_.add(promise);
}
/**
* @param {!ManagedPromise} promise .
*/
clearUnhandledRejection(promise) {
var deleted = this.unhandledRejections_.delete(promise);
if (deleted) {
// TODO: node 4.0.0+
vlog(2, () => this + ' clearing unhandled rejection: ' + promise, this);
}
}
/**
* Enqueues a new task for execution.
* @param {!Task} task The task to enqueue.
* @throws {Error} If this instance has already started execution.
*/
enqueue(task) {
if (this.state_ !== TaskQueueState.NEW) {
throw Error('TaskQueue has started: ' + this);
}
if (task.queue) {
throw Error('Task is already scheduled in another queue');
}
this.tasks_.push(task);
task.queue = this;
ON_CANCEL_HANDLER.set(
task.promise,
(e) => this.onTaskCancelled_(task, e));
vlog(1, () => this + '.enqueue(' + task + ')', this);
vlog(2, () => this.flow_.toString(), this);
}
/**
* Schedules the callbacks registered on the given promise in this queue.
*
* @param {!ManagedPromise} promise the promise whose callbacks should be
* registered as interrupts in this task queue.
* @throws {Error} if this queue has already finished.
*/
scheduleCallbacks(promise) {
if (this.state_ === TaskQueueState.FINISHED) {
throw new Error('cannot interrupt a finished q(' + this + ')');
}
if (this.pending_ && this.pending_.task.promise === promise) {
this.pending_.task.promise.queue_ = null;
this.pending_ = null;
asyncRun(() => this.executeNext_());
}
if (!promise.callbacks_) {
return;
}
promise.callbacks_.forEach(function(cb) {
cb.blocked = false;
if (cb.queue) {
return;
}
ON_CANCEL_HANDLER.set(
cb.promise,
(e) => this.onTaskCancelled_(cb, e));
if (cb.queue === this && this.tasks_.indexOf(cb) !== -1) {
return;
}
if (cb.queue) {
cb.queue.dropTask_(cb);
}
cb.queue = this;
if (!this.interrupts_) {
this.interrupts_ = [];
}
this.interrupts_.push(cb);
}, this);
promise.callbacks_ = null;
vlog(2, () => this + ' interrupted\n' + this.flow_, this);
}
/**
* Starts executing tasks in this queue. Once called, no further tasks may
* be {@linkplain #enqueue() enqueued} with this instance.
*
* @throws {Error} if this queue has already been started.
*/
start() {
if (this.state_ !== TaskQueueState.NEW) {
throw new Error('TaskQueue has already started');
}
// Always asynchronously execute next, even if there doesn't look like
// there is anything in the queue. This will catch pending unhandled
// rejections that were registered before start was called.
asyncRun(() => this.executeNext_());
}
/**
* Aborts this task queue. If there are any scheduled tasks, they are silently
* cancelled and discarded (their callbacks will never fire). If this queue
* has a _pending_ task, the abortion error is used to cancel that task.
* Otherwise, this queue will emit an error event.
*
* @param {*} error The abortion reason.
* @private
*/
abort_(error) {
var cancellation;
if (error instanceof FlowResetError) {
cancellation = error;
} else {
cancellation = new DiscardedTaskError(error);
}
if (this.interrupts_ && this.interrupts_.length) {
this.interrupts_.forEach((t) => t.reject(cancellation));
this.interrupts_ = [];
}
if (this.tasks_ && this.tasks_.length) {
this.tasks_.forEach((t) => t.reject(cancellation));
this.tasks_ = [];
}
// Now that all of the remaining tasks have been silently cancelled (e.g. no
// exisitng callbacks on those tasks will fire), clear the silence bit on
// the cancellation error. This ensures additional callbacks registered in
// the future will actually execute.
cancellation.silent_ = false;
if (this.pending_) {
vlog(2, () => this + '.abort(); cancelling pending task', this);
this.pending_.task.promise.cancel(
/** @type {!CancellationError} */(error));
} else {
vlog(2, () => this + '.abort(); emitting error event', this);
this.emit('error', error, this);
}
}
/** @private */
executeNext_() {
if (this.state_ === TaskQueueState.FINISHED) {
return;
}
this.state_ = TaskQueueState.STARTED;
if (this.pending_ !== null || this.processUnhandledRejections_()) {
return;
}
var task;
do {
task = this.getNextTask_();
} while (task && !isPending(task.promise));
if (!task) {
this.state_ = TaskQueueState.FINISHED;
this.tasks_ = [];
this.interrupts_ = null;
vlog(2, () => this + '.emit(end)', this);
this.emit('end', this);
return;
}
let result = undefined;
this.subQ_ = new TaskQueue(this.flow_);
this.subQ_.once('end', () => { // On task completion.
this.subQ_ = null;
this.pending_ && this.pending_.task.fulfill(result);
});
this.subQ_.once('error', e => { // On task failure.
this.subQ_ = null;
if (Thenable.isImplementation(result)) {
result.cancel(CancellationError.wrap(e));
}
this.pending_ && this.pending_.task.reject(e);
});
vlog(2, () => `${this} created ${this.subQ_} for ${task}`);
try {
this.pending_ = {task: task, q: this.subQ_};
task.promise.queue_ = this;
result = this.subQ_.execute_(task.execute);
this.subQ_.start();
} catch (ex) {
this.subQ_.abort_(ex);
}
}
/**
* @param {!Function} fn .
* @return {T} .
* @template T
* @private
*/
execute_(fn) {
try {
activeFlows.push(this.flow_);
this.flow_.activeQueue_ = this;
return fn();
} finally {
this.flow_.activeQueue_ = null;
activeFlows.pop();
}
}
/**
* Process any unhandled rejections registered with this task queue. If there
* is a rejection, this queue will be aborted with the rejection error. If
* there are multiple rejections registered, this queue will be aborted with
* a {@link MultipleUnhandledRejectionError}.
* @return {boolean} whether there was an unhandled rejection.
* @private
*/
processUnhandledRejections_() {
if (!this.unhandledRejections_.size) {
return false;
}
var errors = new Set();
for (var rejection of this.unhandledRejections_) {
errors.add(rejection.value_);
}
this.unhandledRejections_.clear();
var errorToReport = errors.size === 1
? errors.values().next().value
: new MultipleUnhandledRejectionError(errors);
vlog(1, () => this + ' aborting due to unhandled rejections', this);
if (this.flow_.propagateUnhandledRejections_) {
this.abort_(errorToReport);
return true;
} else {
vlog(1, 'error propagation disabled; reporting to control flow');
this.flow_.reportUncaughtException_(errorToReport);
return false;
}
}
/**
* @param {!Task} task The task to drop.
* @private
*/
dropTask_(task) {
var index;
if (this.interrupts_) {
index = this.interrupts_.indexOf(task);
if (index != -1) {
task.queue = null;
this.interrupts_.splice(index, 1);
return;
}
}
index = this.tasks_.indexOf(task);
if (index != -1) {
task.queue = null;
this.tasks_.splice(index, 1);
}
}
/**
* @param {!Task} task The task that was cancelled.
* @param {!CancellationError} reason The cancellation reason.
* @private
*/
onTaskCancelled_(task, reason) {
if (this.pending_ && this.pending_.task === task) {
this.pending_.q.abort_(reason);
} else {
this.dropTask_(task);
}
}
/**
* @return {(Task|undefined)} the next task scheduled within this queue,
* if any.
* @private
*/
getNextTask_() {
var task = undefined;
while (true) {
if (this.interrupts_) {
task = this.interrupts_.shift();
}
if (!task && this.tasks_) {
task = this.tasks_.shift();
}
if (task && task.blocked) {
vlog(2, () => this + ' skipping blocked task ' + task, this);
task.queue = null;
task = null;
// TODO: recurse when tail-call optimization is available in node.
} else {
break;
}
}
return task;
}
};
/**
* The default flow to use if no others are active.
* @type {ControlFlow}
*/
var defaultFlow;
/**
* A stack of active control flows, with the top of the stack used to schedule
* commands. When there are multiple flows on the stack, the flow at index N
* represents a callback triggered within a task owned by the flow at index
* N-1.
* @type {!Array}
*/
var activeFlows = [];
/**
* Changes the default flow to use when no others are active.
* @param {!ControlFlow} flow The new default flow.
* @throws {Error} If the default flow is not currently active.
*/
function setDefaultFlow(flow) {
if (!usePromiseManager()) {
throw Error(
'You may not change set the control flow when the promise'
+' manager is disabled');
}
if (activeFlows.length) {
throw Error('You may only change the default flow while it is active');
}
defaultFlow = flow;
}
/**
* @return {!ControlFlow} The currently active control flow.
* @suppress {checkTypes}
*/
function controlFlow() {
if (!usePromiseManager()) {
return SIMPLE_SCHEDULER;
}
if (activeFlows.length) {
return activeFlows[activeFlows.length - 1];
}
if (!defaultFlow) {
defaultFlow = new ControlFlow;
}
return defaultFlow;
}
/**
* Creates a new control flow. The provided callback will be invoked as the
* first task within the new flow, with the flow as its sole argument. Returns
* a promise that resolves to the callback result.
* @param {function(!ControlFlow)} callback The entry point
* to the newly created flow.
* @return {!Thenable} A promise that resolves to the callback result.
*/
function createFlow(callback) {
var flow = new ControlFlow;
return flow.execute(function() {
return callback(flow);
});
}
/**
* Tests is a function is a generator.
* @param {!Function} fn The function to test.
* @return {boolean} Whether the function is a generator.
*/
function isGenerator(fn) {
return fn.constructor.name === 'GeneratorFunction';
}
/**
* Consumes a {@code GeneratorFunction}. Each time the generator yields a
* promise, this function will wait for it to be fulfilled before feeding the
* fulfilled value back into {@code next}. Likewise, if a yielded promise is
* rejected, the rejection error will be passed to {@code throw}.
*
* __Example 1:__ the Fibonacci Sequence.
*
* promise.consume(function* fibonacci() {
* var n1 = 1, n2 = 1;
* for (var i = 0; i < 4; ++i) {
* var tmp = yield n1 + n2;
* n1 = n2;
* n2 = tmp;
* }
* return n1 + n2;
* }).then(function(result) {
* console.log(result); // 13
* });
*
* __Example 2:__ a generator that throws.
*
* promise.consume(function* () {
* yield promise.delayed(250).then(function() {
* throw Error('boom');
* });
* }).catch(function(e) {
* console.log(e.toString()); // Error: boom
* });
*
* @param {!Function} generatorFn The generator function to execute.
* @param {Object=} opt_self The object to use as "this" when invoking the
* initial generator.
* @param {...*} var_args Any arguments to pass to the initial generator.
* @return {!Thenable>} A promise that will resolve to the
* generator's final result.
* @throws {TypeError} If the given function is not a generator.
*/
function consume(generatorFn, opt_self, ...var_args) {
if (!isGenerator(generatorFn)) {
throw new TypeError('Input is not a GeneratorFunction: ' +
generatorFn.constructor.name);
}
let ret;
return ret = createPromise((resolve, reject) => {
let generator = generatorFn.apply(opt_self, var_args);
callNext();
/** @param {*=} opt_value . */
function callNext(opt_value) {
pump(generator.next, opt_value);
}
/** @param {*=} opt_error . */
function callThrow(opt_error) {
pump(generator.throw, opt_error);
}
function pump(fn, opt_arg) {
if (ret instanceof ManagedPromise && !isPending(ret)) {
return; // Defererd was cancelled; silently abort.
}
try {
var result = fn.call(generator, opt_arg);
} catch (ex) {
reject(ex);
return;
}
if (result.done) {
resolve(result.value);
return;
}
asap(result.value, callNext, callThrow);
}
});
}
// PUBLIC API
module.exports = {
CancellableThenable: CancellableThenable,
CancellationError: CancellationError,
ControlFlow: ControlFlow,
Deferred: Deferred,
MultipleUnhandledRejectionError: MultipleUnhandledRejectionError,
Thenable: Thenable,
Promise: ManagedPromise,
Scheduler: Scheduler,
all: all,
asap: asap,
captureStackTrace: captureStackTrace,
checkedNodeCall: checkedNodeCall,
consume: consume,
controlFlow: controlFlow,
createFlow: createFlow,
defer: defer,
delayed: delayed,
filter: filter,
finally: thenFinally,
fulfilled: fulfilled,
fullyResolved: fullyResolved,
isGenerator: isGenerator,
isPromise: isPromise,
map: map,
rejected: rejected,
setDefaultFlow: setDefaultFlow,
when: when,
/**
* Indicates whether the promise manager is currently enabled. When disabled,
* attempting to use the {@link ControlFlow} or {@link ManagedPromise Promise}
* classes will generate an error.
*
* The promise manager is currently enabled by default, but may be disabled
* by setting the environment variable `SELENIUM_PROMISE_MANAGER=0` or by
* setting this property to false. Setting this property will always take
* precedence ove the use of the environment variable.
*
* @return {boolean} Whether the promise manager is enabled.
* @see
*/
get USE_PROMISE_MANAGER() { return usePromiseManager(); },
set USE_PROMISE_MANAGER(/** boolean */value) { USE_PROMISE_MANAGER = value; },
get LONG_STACK_TRACES() { return LONG_STACK_TRACES; },
set LONG_STACK_TRACES(v) { LONG_STACK_TRACES = v; },
};