EvertonSilva / Promises

DOM Promises IDL/polyfill

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

NOTE: The IDL hosted here is now advisory. This repo hosts refactoring examples and designs, a high-fidelity polyfill, and rationale for this work. See the living DOM spec for the current version if you are implementing Promises in a runtime.

Promises/A+ logo, since DOMFutures are compatible

DOM Promises

A Promises Design for DOM, currently in IDL. Also a p(r)ollyfill and re-worked APIs to take advantage of the new semantics.

A 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:

  • Promises 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 Promises).
  • Promises 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.
  • Promises aren't, in themselves notable. 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, Promises 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);

// .catch() gives you access only to errors:
futureInstance.catch(onreject);

OK, So Why Promises For DOM?

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.

OK, But Aren't You Late To This Party?

There's a long, long history of Promises both inside and outside JavaScript. Many other languages provide them via language syntax or standard library. Promises are such a common pattern inside JavaScript that nearly all major libraries provide some form them and vend them for many common operations which they wrap. There are 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 Promises, it will also need an answer to those questions. That's what this repository is about.

More Examples

// 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);
});

Promises 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 Promise that represents the fetch:
  return new Promise(function(resolver){
    // The resolver is how a Promise is satisfied. It has reject(), fulfill(),
    // 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.resolve(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) { ... });

About

DOM Promises IDL/polyfill