diff options
Diffstat (limited to 'node_modules/selenium-webdriver/lib/promise.js')
-rw-r--r-- | node_modules/selenium-webdriver/lib/promise.js | 927 |
1 files changed, 588 insertions, 339 deletions
diff --git a/node_modules/selenium-webdriver/lib/promise.js b/node_modules/selenium-webdriver/lib/promise.js index b98e7cf2e..32d0c98e6 100644 --- a/node_modules/selenium-webdriver/lib/promise.js +++ b/node_modules/selenium-webdriver/lib/promise.js @@ -17,6 +17,42 @@ /** * @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 @@ -592,6 +628,7 @@ 'use strict'; +const error = require('./error'); const events = require('./events'); const logging = require('./logging'); @@ -643,7 +680,6 @@ function asyncRun(fn) { }); } - /** * @param {number} level What level of verbosity to log with. * @param {(string|function(this: T): string)} loggable The message to log. @@ -799,32 +835,56 @@ class MultipleUnhandledRejectionError extends Error { * @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. * - * @interface + * @record * @extends {IThenable<T>} * @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. This function - * will also ensure the prototype's {@code then} function is exported from - * compiled code. + * instances of that class implement the Thenable interface. * @param {function(new: Thenable, ...?)} ctor The * constructor whose prototype to modify. */ static addImplementation(ctor) { - ctor.prototype['then'] = ctor.prototype.then; - try { - ctor.prototype[IMPLEMENTED_BY_SYMBOL] = true; - } catch (ignored) { - // Property access denied? - } + addMarkerSymbol(ctor, IMPLEMENTED_BY_SYMBOL); } /** @@ -835,30 +895,10 @@ class Thenable { * interface. */ static isImplementation(object) { - if (!object) { - return false; - } - try { - return !!object[IMPLEMENTED_BY_SYMBOL]; - } catch (e) { - return false; // Property access seems to be forbidden. - } + return hasMarkerSymbol(object, IMPLEMENTED_BY_SYMBOL); } /** - * Cancels 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) {} - - /** @return {boolean} Whether this promise's value is still being computed. */ - isPending() {} - - /** * Registers listeners for when this instance is resolved. * * @param {?(function(T): (R|IThenable<R>))=} opt_callback The @@ -867,8 +907,8 @@ class Thenable { * @param {?(function(*): (R|IThenable<R>))=} opt_errback * The function to call if this promise is rejected. The function should * expect a single argument: the rejection reason. - * @return {!ManagedPromise<R>} A new promise which will be - * resolved with the result of the invoked callback. + * @return {!Thenable<R>} A new promise which will be resolved with the result + * of the invoked callback. * @template R */ then(opt_callback, opt_errback) {} @@ -892,49 +932,53 @@ class Thenable { * @param {function(*): (R|IThenable<R>)} errback The * function to call if this promise is rejected. The function should * expect a single argument: the rejection reason. - * @return {!ManagedPromise<R>} A new promise which will be - * resolved with the result of the invoked callback. + * @return {!Thenable<R>} 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<T>} + * @template T + * @package + */ +class CancellableThenable { /** - * Registers a listener to invoke when this 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 - * } - * - * promise.rejected(Error('one')) - * .finally(function() { - * throw Error('two'); // Hides Error: one - * }); + * @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 {function(): (R|IThenable<R>)} callback The function to call when - * this promise is resolved. - * @return {!ManagedPromise<R>} A promise that will be fulfilled - * with the callback result. - * @template R + * @param {(string|Error)=} opt_reason The reason this promise is being + * cancelled. This value will be wrapped in a {@link CancellationError}. */ - finally(callback) {} + cancel(opt_reason) {} } @@ -967,7 +1011,7 @@ const ON_CANCEL_HANDLER = new WeakMap; * fulfilled or rejected state, at which point the promise is considered * resolved. * - * @implements {Thenable<T>} + * @implements {CancellableThenable<T>} * @template T * @see http://promises-aplus.github.io/promises-spec/ */ @@ -983,6 +1027,12 @@ class ManagedPromise { * 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} */ @@ -1024,6 +1074,30 @@ class ManagedPromise { } } + /** + * Creates a promise that is immediately resolved with the given value. + * + * @param {T=} opt_value The value to resolve. + * @return {!ManagedPromise<T>} 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) + @@ -1176,7 +1250,7 @@ class ManagedPromise { } if (this.parent_ && canCancel(this.parent_)) { - this.parent_.cancel(opt_reason); + /** @type {!CancellableThenable} */(this.parent_).cancel(opt_reason); } else { var reason = CancellationError.wrap(opt_reason); let onCancel = ON_CANCEL_HANDLER.get(this); @@ -1194,7 +1268,7 @@ class ManagedPromise { function canCancel(promise) { if (!(promise instanceof ManagedPromise)) { - return Thenable.isImplementation(promise); + return CancellableThenable.isImplementation(promise); } return promise.state_ === PromiseState.PENDING || promise.state_ === PromiseState.BLOCKED; @@ -1202,11 +1276,6 @@ class ManagedPromise { } /** @override */ - isPending() { - return this.state_ === PromiseState.PENDING; - } - - /** @override */ then(opt_callback, opt_errback) { return this.addCallback_( opt_callback, opt_errback, 'then', ManagedPromise.prototype.then); @@ -1218,21 +1287,15 @@ class ManagedPromise { null, errback, 'catch', ManagedPromise.prototype.catch); } - /** @override */ + /** + * @param {function(): (R|IThenable<R>)} callback + * @return {!ManagedPromise<R>} + * @template R + * @see ./promise.finally() + */ finally(callback) { - var error; - var mustThrow = false; - return this.then(function() { - return callback(); - }, function(err) { - error = err; - mustThrow = true; - return callback(); - }).then(function() { - if (mustThrow) { - throw error; - } - }); + let result = thenFinally(this, callback); + return /** @type {!ManagedPromise} */(result); } /** @@ -1308,7 +1371,16 @@ class ManagedPromise { } } } -Thenable.addImplementation(ManagedPromise); +CancellableThenable.addImplementation(ManagedPromise); + + +/** + * @param {!ManagedPromise} promise + * @return {boolean} + */ +function isPending(promise) { + return promise.state_ === PromiseState.PENDING; +} /** @@ -1405,19 +1477,11 @@ function isPromise(value) { * 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 {!ManagedPromise} The promise. + * @return {!Thenable} The promise. */ function delayed(ms) { - var key; - return new ManagedPromise(function(fulfill) { - key = setTimeout(function() { - key = null; - fulfill(); - }, ms); - }).catch(function(e) { - clearTimeout(key); - key = null; - throw e; + return createPromise(resolve => { + setTimeout(() => resolve(), ms); }); } @@ -1436,15 +1500,11 @@ function defer() { * Creates a promise that has been resolved with the given value. * @param {T=} opt_value The resolved value. * @return {!ManagedPromise<T>} The resolved promise. + * @deprecated Use {@link ManagedPromise#resolve Promise.resolve(value)}. * @template T */ function fulfilled(opt_value) { - if (opt_value instanceof ManagedPromise) { - return opt_value; - } - return new ManagedPromise(function(fulfill) { - fulfill(opt_value); - }); + return ManagedPromise.resolve(opt_value); } @@ -1452,16 +1512,11 @@ function fulfilled(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<T>} The rejected promise. - * @template T + * @return {!ManagedPromise<?>} The rejected promise. + * @deprecated Use {@link ManagedPromise#reject Promise.reject(reason)}. */ function rejected(opt_reason) { - if (opt_reason instanceof ManagedPromise) { - return opt_reason; - } - return new ManagedPromise(function(_, reject) { - reject(opt_reason); - }); + return ManagedPromise.reject(opt_reason); } @@ -1474,12 +1529,12 @@ function rejected(opt_reason) { * @param {!Function} fn The function to wrap. * @param {...?} var_args The arguments to apply to the function, excluding the * final callback. - * @return {!ManagedPromise} A promise that will be resolved with the + * @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 new ManagedPromise(function(fulfill, reject) { + return createPromise(function(fulfill, reject) { try { args.push(function(error, value) { error ? reject(error) : fulfill(value); @@ -1491,6 +1546,59 @@ function checkedNodeCall(fn, var_args) { }); } +/** + * 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<R>)} callback The function to call when + * the promise is resolved. + * @return {!IThenable<R>} 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 @@ -1501,16 +1609,15 @@ function checkedNodeCall(fn, var_args) { * resolved successfully. * @param {Function=} opt_errback The function to call when the value is * rejected. - * @return {!ManagedPromise} A new promise. + * @return {!Thenable} A new promise. */ function when(value, opt_callback, opt_errback) { if (Thenable.isImplementation(value)) { return value.then(opt_callback, opt_errback); } - return new ManagedPromise(function(fulfill) { - fulfill(value); - }).then(opt_callback, opt_errback); + return createPromise(resolve => resolve(value)) + .then(opt_callback, opt_errback); } @@ -1542,14 +1649,14 @@ function asap(value, callback, opt_errback) { * * @param {!Array<(T|!ManagedPromise<T>)>} arr An array of * promises to wait on. - * @return {!ManagedPromise<!Array<T>>} A promise that is + * @return {!Thenable<!Array<T>>} 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 new ManagedPromise(function(fulfill, reject) { + return createPromise(function(fulfill, reject) { var n = arr.length; var values = []; @@ -1603,12 +1710,12 @@ function all(arr) { * @template TYPE, SELF */ function map(arr, fn, opt_self) { - return fulfilled(arr).then(function(v) { + return createPromise(resolve => resolve(arr)).then(v => { if (!Array.isArray(v)) { throw TypeError('not an array'); } var arr = /** @type {!Array} */(v); - return new ManagedPromise(function(fulfill, reject) { + return createPromise(function(fulfill, reject) { var n = arr.length; var values = new Array(n); (function processNext(i) { @@ -1661,12 +1768,12 @@ function map(arr, fn, opt_self) { * @template TYPE, SELF */ function filter(arr, fn, opt_self) { - return fulfilled(arr).then(function(v) { + return createPromise(resolve => resolve(arr)).then(v => { if (!Array.isArray(v)) { throw TypeError('not an array'); } var arr = /** @type {!Array} */(v); - return new ManagedPromise(function(fulfill, reject) { + return createPromise(function(fulfill, reject) { var n = arr.length; var values = []; var valuesLength = 0; @@ -1714,7 +1821,7 @@ function filter(arr, fn, opt_self) { * promise.fullyResolved(value); // Stack overflow. * * @param {*} value The value to fully resolve. - * @return {!ManagedPromise} A promise for a fully resolved version + * @return {!Thenable} A promise for a fully resolved version * of the input value. */ function fullyResolved(value) { @@ -1728,7 +1835,7 @@ function fullyResolved(value) { /** * @param {*} value The value to fully resolve. If a promise, assumed to * already be resolved. - * @return {!ManagedPromise} A promise for a fully resolved version + * @return {!Thenable} A promise for a fully resolved version * of the input value. */ function fullyResolveValue(value) { @@ -1755,13 +1862,13 @@ function fullyResolveValue(value) { return fullyResolveKeys(/** @type {!Object} */ (value)); } - return fulfilled(value); + return createPromise(resolve => resolve(value)); } /** * @param {!(Array|Object)} obj the object to resolve. - * @return {!ManagedPromise} A promise that will be resolved with the + * @return {!Thenable} A promise that will be resolved with the * input object once all of its values have been fully resolved. */ function fullyResolveKeys(obj) { @@ -1773,8 +1880,9 @@ function fullyResolveKeys(obj) { } return n; })(); + if (!numKeys) { - return fulfilled(obj); + return createPromise(resolve => resolve(obj)); } function forEachProperty(obj, fn) { @@ -1788,7 +1896,7 @@ function fullyResolveKeys(obj) { } var numResolved = 0; - return new ManagedPromise(function(fulfill, reject) { + return createPromise(function(fulfill, reject) { var forEachKey = isArray ? forEachElement: forEachProperty; forEachKey(obj, function(partialValue, key) { @@ -1822,6 +1930,236 @@ function fullyResolveKeys(obj) { ////////////////////////////////////////////////////////////////////////////// +/** + * 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<T>)} fn The function to call to start the + * task. + * @param {string=} opt_description A description of the task for debugging + * purposes. + * @return {!Thenable<T>} 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<T>|Thenable|null)=), + * function(*=))} resolver + * @return {!Thenable<T>} + * @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<void>} 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<T>|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<T>} 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<T>|Thenable|null)=), + * function(*=))} resolver + * @return {!Thenable<T>} + * @template T + */ +function createPromise(resolver) { + let ctor = usePromiseManager() ? ManagedPromise : NativePromise; + return new ctor(resolver); +} + + +/** + * @param {!Scheduler} scheduler The scheduler to use. + * @param {(!IThenable<T>|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<T>} 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 || '<anonymous wait: promise resolution>'); + } + + 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 || '<anonymous wait>'); +} + + +/** + * 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 @@ -1848,13 +2186,20 @@ function fullyResolveKeys(obj) { * 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 fora detailed + * 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} */ @@ -2024,21 +2369,7 @@ class ControlFlow extends events.EventEmitter { return this.activeQueue_; } - /** - * Schedules a task for execution. If there is nothing currently in the - * queue, the task will be executed in the next turn of the event loop. If - * the task function is a generator, the task will be executed using - * {@link ./promise.consume consume()}. - * - * @param {function(): (T|ManagedPromise<T>)} fn The function to - * call to start the task. If the function returns a - * {@link ManagedPromise}, this instance will wait for it to be - * resolved before starting the next task. - * @param {string=} opt_description A description of the task. - * @return {!ManagedPromise<T>} A promise that will be resolved - * with the result of the action. - * @template T - */ + /** @override */ execute(fn, opt_description) { if (isGenerator(fn)) { let original = fn; @@ -2060,126 +2391,21 @@ class ControlFlow extends events.EventEmitter { return task.promise; } - /** - * Inserts a {@code setTimeout} into the command queue. This is equivalent to - * a thread sleep in a synchronous programming language. - * - * @param {number} ms The timeout delay, in milliseconds. - * @param {string=} opt_description A description to accompany the timeout. - * @return {!ManagedPromise} A promise that will be resolved with - * the result of the action. - */ + /** @override */ + promise(resolver) { + return new ManagedPromise(resolver, this); + } + + /** @override */ timeout(ms, opt_description) { - return this.execute(function() { - return delayed(ms); + return this.execute(() => { + return this.promise(resolve => setTimeout(() => resolve(), ms)); }, opt_description); } - /** - * Schedules a task that shall wait for a condition to hold. Each condition - * function may return any value, but it will always be evaluated as a - * boolean. - * - * Condition functions may schedule sub-tasks with this instance, however, - * their execution time will be factored into whether a wait has timed out. - * - * In the event a condition returns a ManagedPromise, the polling loop will wait for - * it to be resolved before evaluating whether the condition has been - * satisfied. The resolution time for a promise is factored into whether a - * wait has timed out. - * - * If the condition function throws, or returns a rejected promise, the - * wait task will fail. - * - * If the condition is defined as a promise, the flow 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, the flow will wait indefinitely for the condition to be satisfied. - * - * @param {(!ManagedPromise<T>|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 {!ManagedPromise<T>} 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 - */ + /** @override */ wait(condition, opt_timeout, opt_message) { - var timeout = opt_timeout || 0; - if (typeof timeout !== 'number' || timeout < 0) { - throw TypeError('timeout must be a number >= 0: ' + timeout); - } - - if (isPromise(condition)) { - return this.execute(function() { - if (!timeout) { - return condition; - } - return new ManagedPromise(function(fulfill, reject) { - var start = Date.now(); - var timer = setTimeout(function() { - timer = null; - reject(Error((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 || '<anonymous wait: promise resolution>'); - } - - 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); - } - - var self = this; - return this.execute(function() { - var startTime = Date.now(); - return new ManagedPromise(function(fulfill, reject) { - pollCondition(); - - function pollCondition() { - var conditionFn = /** @type {function()} */(condition); - self.execute(conditionFn).then(function(value) { - var elapsed = Date.now() - startTime; - if (!!value) { - fulfill(value); - } else if (timeout && elapsed >= timeout) { - reject(new Error((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 || '<anonymous wait>'); + return scheduleWait(this, condition, opt_timeout, opt_message); } /** @@ -2510,6 +2736,9 @@ class TaskQueue extends events.EventEmitter { /** @private {({task: !Task, q: !TaskQueue}|null)} */ this.pending_ = null; + /** @private {TaskQueue} */ + this.subQ_ = null; + /** @private {TaskQueueState} */ this.state_ = TaskQueueState.NEW; @@ -2690,7 +2919,7 @@ class TaskQueue extends events.EventEmitter { var task; do { task = this.getNextTask_(); - } while (task && !task.promise.isPending()); + } while (task && !isPending(task.promise)); if (!task) { this.state_ = TaskQueueState.FINISHED; @@ -2701,20 +2930,30 @@ class TaskQueue extends events.EventEmitter { return; } - var self = this; - var subQ = new TaskQueue(this.flow_); - subQ.once('end', () => self.onTaskComplete_(result)) - .once('error', (e) => self.onTaskFailure_(result, e)); - vlog(2, () => self + ' created ' + subQ + ' for ' + task); + 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}`); - var result = undefined; try { - this.pending_ = {task: task, q: subQ}; + this.pending_ = {task: task, q: this.subQ_}; task.promise.queue_ = this; - result = subQ.execute_(task.execute); - subQ.start(); + result = this.subQ_.execute_(task.execute); + this.subQ_.start(); } catch (ex) { - subQ.abort_(ex); + this.subQ_.abort_(ex); } } @@ -2805,28 +3044,6 @@ class TaskQueue extends events.EventEmitter { } /** - * @param {*} value the value originally returned by the task function. - * @private - */ - onTaskComplete_(value) { - if (this.pending_) { - this.pending_.task.fulfill(value); - } - } - - /** - * @param {*} taskFnResult the value originally returned by the task function. - * @param {*} error the error that caused the task function to terminate. - * @private - */ - onTaskFailure_(taskFnResult, error) { - if (Thenable.isImplementation(taskFnResult)) { - taskFnResult.cancel(CancellationError.wrap(error)); - } - this.pending_.task.reject(error); - } - - /** * @return {(Task|undefined)} the next task scheduled within this queue, * if any. * @private @@ -2857,9 +3074,9 @@ class TaskQueue extends events.EventEmitter { /** * The default flow to use if no others are active. - * @type {!ControlFlow} + * @type {ControlFlow} */ -var defaultFlow = new ControlFlow(); +var defaultFlow; /** @@ -2878,6 +3095,11 @@ var activeFlows = []; * @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'); } @@ -2887,10 +3109,21 @@ function setDefaultFlow(flow) { /** * @return {!ControlFlow} The currently active control flow. + * @suppress {checkTypes} */ function controlFlow() { - return /** @type {!ControlFlow} */ ( - activeFlows.length ? activeFlows[activeFlows.length - 1] : defaultFlow); + if (!usePromiseManager()) { + return SIMPLE_SCHEDULER; + } + + if (activeFlows.length) { + return activeFlows[activeFlows.length - 1]; + } + + if (!defaultFlow) { + defaultFlow = new ControlFlow; + } + return defaultFlow; } @@ -2900,8 +3133,7 @@ function controlFlow() { * a promise that resolves to the callback result. * @param {function(!ControlFlow)} callback The entry point * to the newly created flow. - * @return {!ManagedPromise} A promise that resolves to the callback - * result. + * @return {!Thenable} A promise that resolves to the callback result. */ function createFlow(callback) { var flow = new ControlFlow; @@ -2955,53 +3187,51 @@ function isGenerator(fn) { * @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 {!ManagedPromise<?>} A promise that will resolve to the + * @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) { +function consume(generatorFn, opt_self, ...var_args) { if (!isGenerator(generatorFn)) { throw new TypeError('Input is not a GeneratorFunction: ' + generatorFn.constructor.name); } - var deferred = defer(); - var generator = generatorFn.apply( - opt_self, Array.prototype.slice.call(arguments, 2)); - callNext(); - return deferred.promise; - - /** @param {*=} opt_value . */ - function callNext(opt_value) { - pump(generator.next, opt_value); - } - - /** @param {*=} opt_error . */ - function callThrow(opt_error) { - // Dictionary lookup required because Closure compiler's built-in - // externs does not include GeneratorFunction.prototype.throw. - pump(generator['throw'], opt_error); - } + let ret; + return ret = createPromise((resolve, reject) => { + let generator = generatorFn.apply(opt_self, var_args); + callNext(); - function pump(fn, opt_arg) { - if (!deferred.promise.isPending()) { - return; // Defererd was cancelled; silently abort. + /** @param {*=} opt_value . */ + function callNext(opt_value) { + pump(generator.next, opt_value); } - try { - var result = fn.call(generator, opt_arg); - } catch (ex) { - deferred.reject(ex); - return; + /** @param {*=} opt_error . */ + function callThrow(opt_error) { + pump(generator.throw, opt_error); } - if (result.done) { - deferred.fulfill(result.value); - return; - } + function pump(fn, opt_arg) { + if (ret instanceof ManagedPromise && !isPending(ret)) { + return; // Defererd was cancelled; silently abort. + } - asap(result.value, callNext, callThrow); - } + 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); + } + }); } @@ -3009,12 +3239,14 @@ function consume(generatorFn, opt_self, var_args) { module.exports = { + CancellableThenable: CancellableThenable, CancellationError: CancellationError, ControlFlow: ControlFlow, Deferred: Deferred, MultipleUnhandledRejectionError: MultipleUnhandledRejectionError, Thenable: Thenable, Promise: ManagedPromise, + Scheduler: Scheduler, all: all, asap: asap, captureStackTrace: captureStackTrace, @@ -3025,6 +3257,7 @@ module.exports = { defer: defer, delayed: delayed, filter: filter, + finally: thenFinally, fulfilled: fulfilled, fullyResolved: fullyResolved, isGenerator: isGenerator, @@ -3034,6 +3267,22 @@ module.exports = { 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 <https://github.com/SeleniumHQ/selenium/issues/2969> + */ + 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; }, }; |