namespace webdriver.promise
The promise module is centered around the
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
WebDriver API is built on top of the
control flow, allowing users to write
driver.get('http://www.google.com/ncr');
driver.findElement({name: 'q'}).sendKeys('webdriver');
driver.findElement({name: 'btnGn'}).click();
instead of
driver.get('http://www.google.com/ncr')
.then(function() {
return driver.findElement({name: 'q'});
})
.then(function(q) {
return q.sendKeys('webdriver');
})
.then(function() {
return driver.findElement({name: 'btnG'});
})
.then(function(btnG) {
return btnG.click();
});
Tasks and Task Queues
The control flow is based on the concept of tasks and task queues. Tasks are
functions that define the basic unit of work for the control flow to execute.
Each task is scheduled via
ControlFlow#execute()
, which
will return a Promise
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. The control flow will create a new task queue
the first time a task is scheduled within an event loop.
var flow = promise.controlFlow();
flow.execute(foo); // Creates a new task queue and inserts foo.
flow.execute(bar); // Inserts bar into the same queue as foo.
setTimeout(function() {
flow.execute(baz); // Creates a new task queue and inserts baz.
}, 0);
Whenever the control flow creates a new task queue, it will automatically
begin executing tasks in the next available turn of the event loop. This
execution is scheduled using a "micro-task" timer, such as a (native)
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 "micro-task" timers, which have a higher priority than
"macro-tasks" like setTimeout
.
Task Execution
Upon creating a task queue, and whenever an exisiting queue completes a task,
the control flow will schedule a micro-task timer to process any scheduled
tasks. This ensures no task is ever started within the same turn of the
JavaScript event loop in which it was scheduled, nor is a task ever started
within the same turn that another finishes.
When the execution timer fires, a single task will be dequeued and executed.
There are several important events that may occur while executing a task
function:
- A new task queue is created by a call to
ControlFlow#execute()
. Any
tasks scheduled within this task queue are considered subtasks of the
current task. - The task function throws an error. Any scheduled tasks are immediately
discarded and the task's promised result (previously returned by
ControlFlow#execute()
) is
immediately rejected with the thrown error. - The task function returns sucessfully.
If a task function created a new task queue, the control flow will wait for
that queue to complete before processing the task result. If the queue
completes without error, the flow will settle the task's promise with the
value originaly returned by the task function. On the other hand, if the task
queue termintes with an error, the task's promise will be rejected with that
error.
flow.execute(function() {
flow.execute(() => console.log('a'));
flow.execute(() => console.log('b'));
});
flow.execute(() => console.log('c'));
// a
// b
// c
Promise Integration
In addition to the ControlFlow
class,
the promise module also exports a Promise/A+
implementation that is deeply
integrated with the ControlFlow. First and foremost, each promise
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 Promise Callbacks
How callbacks are scheduled in the control flow depends on when they are
attached to the promise. Callbacks attached to a previously resolved
promise are immediately enqueued as subtasks of the currently running task.
var p = promise.fulfilled();
flow.execute(function() {
flow.execute(() => console.log('A'));
p.then( () => console.log('B'));
flow.execute(() => console.log('C'));
p.then( () => console.log('D'));
}).then(function() {
console.log('fin');
});
// A
// B
// C
// D
// fin
When a promise is resolved while a task function is on the call stack, any
callbacks also registered in that stack frame are scheduled as if the promise
were already resolved:
var d = promise.defer();
flow.execute(function() {
flow.execute( () => console.log('A'));
d.promise.then(() => console.log('B'));
flow.execute( () => console.log('C'));
d.promise.then(() => console.log('D'));
d.fulfill();
}).then(function() {
console.log('fin');
});
// A
// B
// C
// D
// fin
If a promise is resolved while a task function is on the call stack, any
previously registered callbacks (i.e. attached while the task was not on
the call stack), 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() {
flow.execute(() => console.log('C'));
flow.execute(() => console.log('D'));
d1.fulfill();
d2.fulfill();
}).then(function() {
console.log('fin');
});
// A
// B
// C
// D
// fin
Within a task function (or callback), each step of a promise chain acts as
an interrupt on the task queue:
var d = promise.defer();
flow.execute(function() {
d.promise.
then(() => console.log('A')).
then(() => console.log('B')).
then(() => console.log('C')).
then(() => console.log('D'));
flow.execute(() => console.log('E'));
d.fulfill();
}).then(function() {
console.log('fin');
});
// A
// B
// C
// D
// E
// fin
If there are multiple promise chains derived from a single promise, they are
processed in the order created:
var d = promise.defer();
flow.execute(function() {
var chain = d.promise.then(() => console.log('A'));
chain.then(() => console.log('B')).
then(() => console.log('C'));
chain.then(() => console.log('D')).
then(() => console.log('E'));
flow.execute(() => console.log('F'));
d.fulfill();
}).then(function() {
console.log('fin');
});
// A
// B
// C
// D
// E
// F
// fin
Even though a subtask's promised result will never resolve while the task
function is on the stack, it will be treated as a promise resolved within the
task. In all other scenarios, a task's promise behaves just like a normal
promise. In the sample below, C/D
is loggged before B
because the
resolution of subtask1
interrupts the flow of the enclosing task. Within
the final subtask, E/F
is logged in order because subtask1
is a resolved
promise when that task runs.
flow.execute(function() {
var subtask1 = flow.execute(() => console.log('A'));
var subtask2 = flow.execute(() => console.log('B'));
subtask1.then(() => console.log('C'));
subtask1.then(() => console.log('D'));
flow.execute(function() {
flow.execute(() => console.log('E'));
subtask1.then(() => console.log('F'));
});
}).then(function() {
console.log('fin');
});
// A
// C
// D
// B
// E
// F
// fin
Note: while the ControlFlow will wait for
tasks and
callbacks to complete, it
will not wait for unresolved promises created within a task:
flow.execute(function() {
var p = new promise.Promise(function(fulfill) {
setTimeout(fulfill, 100);
});
p.then(() => console.log('promise resolved!'));
}).then(function() {
console.log('task complete!');
});
// task complete!
// promise resolved!
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 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
Promise is rejected and no callbacks
are defined on that promise, it is considered an unhandled rejection and
reproted to the active task queue. If the rejection remains unhandled after
a single turn of the event loop (scheduled with a micro-task), it
will propagate up the stack.
Error Propagation
If an unhandled rejection occurs within a task function, that task's promised
result is rejected and all remaining subtasks are discarded:
flow.execute(function() {
// No callbacks registered on promise -> unhandled rejection
promise.rejected(Error('boom'));
flow.execute(function() { console.log('this will never run'); });
}).thenCatch(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'); });
}).thenCatch(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);
}).thenCatch(function(e) {
console.log(e.message);
});
// boom
If there are multiple unhandled rejections within a task, they are packaged
in a 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'));
}).thenCatch(function(ex) {
console.log(ex instanceof promise.MultipleUnhandledRejectionError);
for (var e of ex.errors) {
console.log(e.message);
}
});
// boom1
// boom2
When a subtask is discarded due to an unreported rejection in its parent
frame, the existing callbacks on that task will never settle and the
callbacks will not be invoked. If a new callback is attached ot the subtask
after it has been discarded, it is handled the same as adding a callback
to a cancelled promise: the error-callback path is invoked. This behavior is
intended to handle cases where the user saves a reference to a task promise,
as illustrated below.
var subTask;
flow.execute(function() {
promise.rejected(Error('boom'));
subTask = flow.execute(function() {});
}).thenCatch(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!');
});
});
}).thenCatch(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
object in node, or
window.onerror
when running in a browser.
Bottom line: you must handle rejected promises.
Promise/A+ Compatibility
This promise
module is compliant with the Promise/A+ specification
except for sections 2.2.6.1
and 2.2.6.2
:
then
may be called multiple times on the same promise.
- If/when
promise
is fulfilled, all respective onFulfilled
callbacks
must execute in the order of their originating calls to then
. - If/when
promise
is rejected, all respective onRejected
callbacks
must execute in the order of their originating calls to then
.
Specifically, the conformance tests contains the following scenario (for
brevity, only the fulfillment version is shown):
var p1 = Promise.resolve();
p1.then(function() {
console.log('A');
p1.then(() => console.log('B'));
});
p1.then(() => console.log('C'));
// A
// C
// B
Since the ControlFlow 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
Functions
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.
Returns
webdriver.promise.Promise<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.
asap(value, callback, opt_errback)code »
Invokes the appropriate callback function as soon as a promised
value
is resolved. This function is similar to
webdriver.promise.when
, except it does not return a new promise.
Parameters
- value
*
The value to observe.
- callback
Function
The function to call when the value is
resolved successfully.
- opt_errback
?Function=
The function to call when the value is
rejected.
captureStackTrace(name, msg, topFn)code »
Generates an error to capture the current stack trace.
Parameters
- name
string
Error name for this stack trace.
- msg
string
Message to record.
- topFn
Function
The function that should appear at the top of the
stack; only applicable in V8.
Returns
Error
The generated error.
checkedNodeCall(fn, var_args)code »
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.
Parameters
- fn
Function
The function to wrap.
- var_args
...?
The arguments to apply to the function, excluding the
final callback.
consume(generatorFn, opt_self, var_args)code »
Consumes a GeneratorFunction
. Each time the generator yields a
promise, this function will wait for it to be fulfilled before feeding the
fulfilled value back into next
. Likewise, if a yielded promise is
rejected, the rejection error will be passed to 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');
});
}).thenCatch(function(e) {
console.log(e.toString()); // Error: boom
});
Parameters
- generatorFn
Function
The generator function to execute.
- opt_self
?Object=
The object to use as "this" when invoking the
initial generator.
- var_args
...*
Any arguments to pass to the initial generator.
Throws
TypeError
If the given function is not a generator.
createFlow(callback)code »
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.
Creates a new deferred object.
Creates a promise that will be resolved at a set time in the future.
Parameters
- ms
number
The amount of time, in milliseconds, to wait before
resolving the promise.
<TYPE, SELF>
filter(arr, fn, opt_self)code »
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.
<T>
fulfilled(opt_value)code »
Creates a promise that has been resolved with the given value.
Parameters
- opt_value
?T=
The resolved value.
fullyResolved(value)code »
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 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.
Parameters
- value
*
The value to fully resolve.
isGenerator(fn)code »
Tests is a function is a generator.
Parameters
- fn
Function
The function to test.
Returns
boolean
Whether the function is a generator.
isPromise(value)code »
Determines whether a value
should be treated as a promise.
Any object whose "then" property is a function will be considered a promise.
Parameters
- value
*
The value to test.
Returns
boolean
Whether the value is a promise.
<TYPE, SELF>
map(arr, fn, opt_self)code »
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.
Parameters
- arr
(Array<TYPE>|webdriver.promise.Promise<Array<TYPE>>)
The
array to iterator over, or a promise that will resolve to said array.
- fn
function(this: SELF, TYPE, number, Array<TYPE>): ?
The
function to call for each element in the array. This function should
expect three arguments (the element, the index, and the array itself.
- opt_self
?SELF=
The object to be used as the value of 'this' within
fn
.
<T>
rejected(opt_reason)code »
Creates a promise that has been rejected with the given reason.
Parameters
- opt_reason
*=
The rejection reason; may be any value, but is
usually an Error or a string.
setDefaultFlow(flow)code »
Changes the default flow to use when no others are active.
Throws
Error
If the default flow is not currently active.
when(value, opt_callback, opt_errback)code »
Registers an observer on a promised value
, returning a new promise
that will be resolved when the value is. If value
is not a promise,
then the return promise will be immediately resolved.
Parameters
- value
*
The value to observe.
- opt_callback
?Function=
The function to call when the value is
resolved successfully.
- opt_errback
?Function=
The function to call when the value is
rejected.
Compiler Constants
Types
- CancellationError
Error used when the computation of a promise is cancelled.
- ControlFlow
Handles the execution of scheduled tasks, each of which may be an
asynchronous operation.
- Deferred
Represents a value that will be resolved at some point in the future.
- MultipleUnhandledRejectionError
Error used when there are multiple unhandled promise rejections detected
within a task or callback.
- Promise
Represents the eventual value of a completed operation.
- Thenable
No description.