// 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 motivation 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} 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 as a microtask][MicrotasksArticle] like e.g. a * (native) `Promise.then()` callback. * * setTimeout(() => console.log('a')); * Promise.resolve().then(() => console.log('b')); // A native promise. * flow.execute(() => console.log('c')); * Promise.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 "microtask" timers, which have a higher priority than * "macrotasks" like `setTimeout`. * * ## Task Execution * * Upon creating a task queue, and whenever an existing queue completes a task, * the control flow will schedule a microtask 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 successfully. * * 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 originally returned by the task function. On the other hand, if the task * queue terminates 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 [Promises/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 logged 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 * Promise class. * * Promise.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 reported to the active task queue. If the rejection * remains unhandled after a single turn of the [event loop][JSEL] (scheduled * with a microtask), 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 to 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. * * # Promises/A+ Compatibility * * This `promise` module is compliant with the [Promises/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 contain the following scenario (for * brevity, only the fulfillment version is shown): * * var p1 = Promise.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* * [Promises/A+]: https://promisesaplus.com/ * [MicrotasksArticle]: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ */ '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 microtask 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 promise-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; const SKIP_LOG = Symbol('skip-log'); const FLOW_LOG = logging.getLogger('promise.ControlFlow'); /** * 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. * @param {?=} opt_skipLog An internal parameter used to skip logging the * creation of this promise. This parameter has no effect unless it is * strictly equal to an internal symbol. In other words, this parameter * is always ignored for external code. */ constructor(resolver, opt_flow, opt_skipLog) { 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']); } else if (opt_skipLog !== SKIP_LOG) { FLOW_LOG.warning(() => { let e = captureStackTrace( 'ManagedPromiseError', 'Creating a new managed Promise. This call will fail when the' + ' promise manager is disabled', ManagedPromise) return e.stack; }); } 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 * result 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; } /** * Structural interface for a deferred promise resolver. * @record * @template T */ function Resolver() {} /** * The promised value for this resolver. * @type {!Thenable} */ Resolver.prototype.promise; /** * Resolves the promised value with the given `value`. * @param {T|Thenable} value * @return {void} */ Resolver.prototype.resolve; /** * Rejects the promised value with the given `reason`. * @param {*} reason * @return {void} */ Resolver.prototype.reject; /** * 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 * @implements {Resolver} */ class Deferred { /** * @param {ControlFlow=} opt_flow The control flow this instance was * created under. This should only be provided during unit tests. * @param {?=} opt_skipLog An internal parameter used to skip logging the * creation of this promise. This parameter has no effect unless it is * strictly equal to an internal symbol. In other words, this parameter * is always ignored for external code. */ constructor(opt_flow, opt_skipLog) { var fulfill, reject; /** @type {!ManagedPromise} */ this.promise = new ManagedPromise(function(f, r) { fulfill = f; reject = r; }, opt_flow, opt_skipLog); 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. * @const */ this.resolve = function(opt_value) { checkNotSelf(opt_value); fulfill(opt_value); }; /** * An alias for {@link #resolve}. * @const */ this.fulfill = this.resolve; /** * 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. * @const */ 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 resolver. * * If the promise manager is currently enabled, this function will return a * {@link Deferred} instance. Otherwise, it will return a resolver for a * {@linkplain NativePromise native promise}. * * @return {!Resolver} A new deferred resolver. * @template T */ function defer() { if (usePromiseManager()) { return new Deferred(); } let resolve, reject; let promise = new NativePromise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); return {promise, resolve, reject}; } /** * Creates a promise that has been resolved with the given value. * * If the promise manager is currently enabled, this function will return a * {@linkplain ManagedPromise managed promise}. Otherwise, it will return a * {@linkplain NativePromise native promise}. * * @param {T=} opt_value The resolved value. * @return {!Thenable} The resolved promise. * @template T */ function fulfilled(opt_value) { let ctor = usePromiseManager() ? ManagedPromise : NativePromise; if (opt_value instanceof ctor) { return /** @type {!Thenable} */(opt_value); } if (usePromiseManager()) { // We can skip logging warnings about creating a managed promise because // this function will automatically switch to use a native promise when // the promise manager is disabled. return new ManagedPromise( resolve => resolve(opt_value), undefined, SKIP_LOG); } return NativePromise.resolve(opt_value); } /** * Creates a promise that has been rejected with the given reason. * * If the promise manager is currently enabled, this function will return a * {@linkplain ManagedPromise managed promise}. Otherwise, it will return a * {@linkplain NativePromise native promise}. * * @param {*=} opt_reason The rejection reason; may be any value, but is * usually an Error or a string. * @return {!Thenable} The rejected promise. */ function rejected(opt_reason) { if (usePromiseManager()) { // We can skip logging warnings about creating a managed promise because // this function will automatically switch to use a native promise when // the promise manager is disabled. return new ManagedPromise( (_, reject) => reject(opt_reason), undefined, SKIP_LOG); } return NativePromise.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. * @deprecated Use `promise.fulfilled(value).then(opt_callback, opt_errback)` */ function when(value, opt_callback, opt_errback) { return fulfilled(value).then(opt_callback, opt_errback); } /** * Invokes the appropriate callback function as soon as a promised `value` is * resolved. * * @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 fulfilled(value).then(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. Promise * 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 satisfied. 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']); } /** * Creates a new promise with the given `resolver` function. If the promise * manager is currently enabled, the returned promise will be a * {@linkplain ManagedPromise} instance. Otherwise, it will be a native promise. * * @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 satisfied. 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 order 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; /** * Microtask 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().execute( * () => promise.rejected('failed'), 'example'); * // 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 control 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 active 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_) { let holdIntervalMs = 2147483647; // 2^31-1; max timer length for Node.js this.hold_ = setInterval(function() {}, holdIntervalMs); } let task = new Task( this, fn, opt_description || '', {name: 'Task', top: ControlFlow.prototype.execute}, true); let q = this.getActiveQueue_(); for (let i = q.tasks_.length; i > 0; i--) { let previousTask = q.tasks_[i - 1]; if (previousTask.userTask_) { FLOW_LOG.warning(() => { return `Detected scheduling of an unchained task. When the promise manager is disabled, unchained tasks will not wait for previously scheduled tasks to finish before starting to execute. New task: ${task.promise.stack_.stack} Previous task: ${previousTask.promise.stack_.stack}`.split(/\n/).join('\n '); }); break; } } q.enqueue(task); this.emit(ControlFlow.EventType.SCHEDULE_TASK, task.description); return task.promise; } /** @override */ promise(resolver) { return new ManagedPromise(resolver, this, SKIP_LOG); } /** @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 microtask 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. * @param {boolean=} opt_isUserTask Whether this task was explicitly scheduled * by the use of the promise manager. */ constructor(flow, fn, description, opt_stackOptions, opt_isUserTask) { super(flow, SKIP_LOG); getUid(this); /** @type {function(): (T|!ManagedPromise)} */ this.execute = fn; /** @type {string} */ this.description = description; /** @type {TaskQueue} */ this.queue = null; /** @private @const {boolean} */ this.userTask_ = !!opt_isUserTask; /** * 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 // existing 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.resolve(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; // Deferred 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, Resolver: Resolver, Scheduler: Scheduler, all: all, asap: asap, captureStackTrace: captureStackTrace, checkedNodeCall: checkedNodeCall, consume: consume, controlFlow: controlFlow, createFlow: createFlow, createPromise: createPromise, 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 over 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; }, };