NOTE: The IDL hosted here is no longer canonical. See the living DOM spec for the current version.
DOM Futures (aka "Promises") Design, currently in IDL. Also a p(r)ollyfill and re-worked APIs to take advantage of the new semantics.
Futures? Promises? I Don't Speak Your Crazy Moon Language.
A Future (aka "Promise") is an object that implements a standard contract for the results of an operation that may have already occurred or may happen in the future.
There's a lot in that sentence that's meaningful, but the big ticket items are:
- Futures are a contract for a single result, either success or failure. If you need to model something that happens multiple times, a single Future is not the answer (try events or an iterator that creates a stream of Futures).
- Futures provide a mechanism for multiple parties to be informed of a value, much the same way many bits of code can hold a reference to a variable without knowing about each other.
- Futures aren't, in themselves valuable. Instead, they derive their value through being the one way to encapsulate potentially asynchronous return values.
To do all of this without syntax support from the language, Futures must be
objects with standardized methods. The current design provides a constructable class that allows end-user code to vend instances of Future
from their own APIs as well as a few standard methods:
// Accepts "accept" and "reject" callbacks, roughly the same thing as success
// and failure for the operation.
futureInstance.then(onaccept, onreject);
// Accept and reject callbacks only ever have a single parameter, which is the
// value or reason, respectively:
futureInstance.then(
function(value) {
doSomethingWithTheResult(value);
},
function(reason) { console.error(reason); }
);
// .then() always returns a new Future, one which takes whatever value the
// callback it's registered with returns:
futureInstance.then(function(value) {
return processValue(value);
})
.then(function(processedValue) {
// ...
});
// A key part of the Futures design is that these operations are *chainable*,
// even for asynchronous code. This makes it easy to compose asynchronous
// operations in readable, linear-looking code:
futureInstance.then(aHandlerThatReturnsAFuture)
.then(doSomethingAfterTheSecondCallback);
// If you're not trying to chain, there's a .done() method which just lets you
// listen in on the results:
futureInstance.done(onaccept, onreject);
// Similarly, .catch() gives you access only to errors:
futureInstance.catch(onreject);
To understand why DOM needs Futures, think about a few of the asynchronous APIs in DOM that return single values:
There are some similarities today in how these APIs work. XHR and onload
share the idea of a readyState
property to let code detect if something has happened in the past, giving rise to logic like this:
if (document.readyState == "complete") {
doStartupWork();
} else {
document.addEventListener("load", doStartupWork, false);
}
This is cumbersome and error-prone, not to mention ugly. IndexedDB's
IDBRequest
class also supports a readyState
property, but the values range from 1-2, not 0-4 as used in XHR or strings as used for documents. Making matters worse, the callback and event names don't even match! Clearly DOM needs a better way to do things.
A uniform interface would allow us to manage our callbacks sanely across APIs:
// Example of what the future might hold, not in any current spec
document.ready().then(doStartupWork);
// By returning a Future subclass, XHR's send() method gets more usable too:
var xhr = new XMLHttpRequest();
xhr.open("GET", filename, true);
xhr.send().then(handleSuccessfulResponse,
handleErrorResponse);
// Geolocation can be easily refactored too:
navigator.geolocation.getCurrentPosition().then(successCallback, errorCallback);
// Even IDB gets saner:
indexedDB.open(databaseName).then(function(db) {
// ...
});
Providing a single abstraction for this sort of operation creates cross-cutting value across specifications, making it easier to use DOM and simpler for libraries to interoperate based on a standard design.
There's a long, long history of Future and Promises both inside and outside JavaScript. Many other languages provide them via language syntax or standard library. Futures are such a common pattern inside JavaScript that nearly all major libraries provide some form of Future or Promise and vend them for many common operations which they wrap. There are many differences in terminology and use, but the core ideas are mostly the same be they jQuery Deferreds, WinJS Promises, Dojo Deferreds or Promises, Cujo Promises, Q Promises, RSVP Promises (used heavily in Ember), or even in Node Promises. The diversity of implementations has led both to incompatibility and efforts to standardize, the most promising of which is the Promises/A+ effort, which of course differs slightly from Promises/A and greatly from other pseudo-standard variants proposed over the years. Promises/A+ doesn't define all of the semantics needed for a full implementation, and if we assume DOM needs Futures/Promises, it will also need an answer to those questions. That's what this repository is about.
// New APIs that vend Futures are easier to reason about. Instead of:
if (document.readyState == "complete") {
doStartupWork();
} else {
document.addEventListener("load", doStartupWork, false);
}
// ...a Future-vending ready() method can be used at any time:
document.ready().then(doStartupWork);
// Like other Promises-style APIs, .then() and .done() are the
// primary way to work with Futures, including via chaining, in
// this example using an API proposed at:
// https://github.com/slightlyoff/async-local-storage
var storage = navigator.storage;
storage.get("item 1").then(function(item1value) {
return storage.set("item 1", "howdy");
}).
done(function() {
// The previous future is chained to not resolve until
//item 1 is set to "howdy"
storage.get("item 1").done(console.log);
});
Futures can also be new'd up and used in your own APIs, making them a powerful abstraction for building asynchronous contracts for single valued operations; basically any time you want to do some work asynchronously but only care about a single response value:
function fetchJSON(filename) {
// Return a Future that represents the fetch:
return new Future(function(resolver){
// The resolver is how a Future is satisfied. It has reject(), accept(),
// and resolve() methods that your code can use to inform listeners with:
var xhr = new XMLHttpRequest();
xhr.open("GET", filename, true);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
try {
resolver.accept(JSON.parse(xhr.responseText));
} catch(e) {
resolver.reject(e);
}
}
}
});
}
// Now we can use the uniform Future API to reason about JSON fetches:
fetchJSON("thinger.json").then(function(object) { ... } ,
function(error) { ... });