| // 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, Key, 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', Key.RETURN); |
| * > 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', Key.RETURN); |
| * |
| * instead of |
| * |
| * driver.get('http://www.google.com/ncr') |
| * .then(function() { |
| * return driver.findElement({name: 'q'}); |
| * }) |
| * .then(function(q) { |
| * return q.sendKeys('webdriver', Key.RETURN); |
| * }); |
| * |
| * ## 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 <a id="scheduling_callbacks"></a> |
| * |
| * 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<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. |
| * @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<R>))=} 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<R>))=} opt_errback |
| * The function to call if this promise is rejected. The function should |
| * expect a single argument: the rejection reason. |
| * @return {!Thenable<R>} 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<R>)} errback The |
| * function to call if this promise is rejected. The function should |
| * expect a single argument: the rejection reason. |
| * @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 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<T>} |
| * @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<!ManagedPromise, function(!CancellationError)>} |
| */ |
| 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<T>} |
| * @template T |
| * @see http://promises-aplus.github.io/promises-spec/ |
| */ |
| class ManagedPromise { |
| /** |
| * @param {function( |
| * function((T|IThenable<T>|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<!Task>} */ |
| 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<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) + |
| ' {[[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<R>)} callback |
| * @return {!ManagedPromise<R>} |
| * @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<R>)|null|undefined)} callback The |
| * fulfillment callback. |
| * @param {(function(*): (R|IThenable<R>)|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<R>} 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<R>)|null|undefined)} callback The |
| * fulfillment callback. |
| * @param {(function(*): (R|IThenable<R>)|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<T>} |
| */ |
| Resolver.prototype.promise; |
| |
| |
| /** |
| * Resolves the promised value with the given `value`. |
| * @param {T|Thenable<T>} 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<T>} |
| */ |
| 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<T>} */ |
| 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<T>|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<T>} 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<T>} 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<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 |
| * 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<T>)>} arr An array of |
| * promises to wait on. |
| * @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 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<TYPE>|ManagedPromise<!Array<TYPE>>)} arr The |
| * array to iterator over, or a promise that will resolve to said array. |
| * @param {function(this: SELF, TYPE, number, !Array<TYPE>): ?} 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<TYPE>|ManagedPromise<!Array<TYPE>>)} arr The |
| * array to iterator over, or a promise that will resolve to said array. |
| * @param {function(this: SELF, TYPE, number, !Array<TYPE>): ( |
| * boolean|ManagedPromise<boolean>)} 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<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. 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<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 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<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 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 || '<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 |
| * 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<TaskQueue>} */ |
| 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 || '<anonymous>', |
| {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<T>)} 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<T>)} */ |
| 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<!Task>} */ |
| this.tasks_ = []; |
| |
| /** @private {Array<!Task>} */ |
| 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<!ManagedPromise>} */ |
| 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<!ControlFlow>} |
| */ |
| 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 <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; }, |
| }; |