Update to latest jsonld.js, add Future API support.
authorDave Longley <dlongley@digitalbazaar.com>
Wed, 24 Apr 2013 17:06:07 -0400
changeset 1601 038a4076510b
parent 1600 c90273f1f126
child 1602 613ff5f4ae2d
Update to latest jsonld.js, add Future API support.

- Use DOM Future API that is a part of new jsonld.js in playground
and in WebIDL test.
playground/Future.js
playground/index.html
playground/jsonld.js
playground/playground.js
test-suite/idltest/index.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/playground/Future.js	Wed Apr 24 17:06:07 2013 -0400
@@ -0,0 +1,392 @@
+// Copyright (C) 2013:
+//    Alex Russell <[email protected]>
+//    Yehuda Katz
+//
+// Use of this source code is governed by
+//    http://www.apache.org/licenses/LICENSE-2.0
+
+// FIXME(slightlyoff):
+//    - Document "npm test"
+//    - Change global name from "Future" to something less conflicty
+(function(global, browserGlobal, underTest) {
+"use strict";
+
+// FIXME(slighltyoff):
+//  * aggregates + tests
+//  * check on fast-forwarding
+
+underTest = !!underTest;
+
+//
+// Async Utilities
+//
+
+// Borrowed from RSVP.js
+var async;
+
+var MutationObserver = browserGlobal.MutationObserver ||
+                       browserGlobal.WebKitMutationObserver;
+var Future;
+
+if (typeof process !== 'undefined' &&
+  {}.toString.call(process) === '[object process]') {
+  async = function(callback, binding) {
+    process.nextTick(function() {
+      callback.call(binding);
+    });
+  };
+} else if (MutationObserver) {
+  var queue = [];
+
+  var observer = new MutationObserver(function() {
+    var toProcess = queue.slice();
+    queue = [];
+
+    toProcess.forEach(function(tuple) {
+      var callback = tuple[0], binding = tuple[1];
+      callback.call(binding);
+    });
+  });
+
+  var element = document.createElement('div');
+  observer.observe(element, { attributes: true });
+
+  // Chrome Memory Leak: https://bugs.webkit.org/show_bug.cgi?id=93661
+  window.addEventListener('unload', function(){
+    observer.disconnect();
+    observer = null;
+  });
+
+  async = function(callback, binding) {
+    queue.push([callback, binding]);
+    element.setAttribute('drainQueue', 'drainQueue');
+  };
+} else {
+  async = function(callback, binding) {
+    setTimeout(function() {
+      callback.call(binding);
+    }, 1);
+  };
+}
+
+//
+// Object Model Utilities
+//
+
+// defineProperties utilities
+var _readOnlyProperty = function(v) {
+    return {
+      enumerable: true,
+      configurable: false,
+      get: v
+    };
+};
+
+var _method = function(v, e, c, w) {
+    return {
+      enumerable:   !!(e || 0),
+      configurable: !!(c || 1),
+      writable:     !!(w || 1),
+      value:           v || function() {}
+    };
+};
+
+var _pseudoPrivate = function(v) { return _method(v, 0, 1, 0); };
+var _public = function(v) { return _method(v, 1); };
+
+//
+// Futures Utilities
+//
+
+var isThenable = function(any) {
+  try {
+    var f = any.then;
+    if (typeof f == "function") {
+      return true;
+    }
+  } catch (e) { /*squelch*/ }
+  return false;
+};
+
+var AlreadyResolved = function(name) {
+  Error.call(this, name);
+};
+AlreadyResolved.prototype = Object.create(Error.prototype);
+
+var Backlog = function() {
+  var bl = [];
+  bl.pump = function(value) {
+    async(function() {
+      var l = bl.length;
+      var x = 0;
+      while(x < l) {
+        x++;
+        bl.shift()(value);
+      }
+    });
+  };
+  return bl;
+};
+
+//
+// Resolver Constuctor
+//
+
+var Resolver = function(future,
+                        acceptCallbacks,
+                        rejectCallbacks,
+                        setValue,
+                        setError,
+                        setState) {
+  var isResolved = false;
+
+  var resolver = this;
+  var accept = function(value) {
+    async(function() {
+      setState("accepted");
+      setValue(value);
+      acceptCallbacks.pump(value);
+    });
+  };
+  var reject = function(reason) {
+    async(function() {
+      setState("rejected");
+      setError(reason);
+      rejectCallbacks.pump(reason);
+    });
+  };
+  var resolve = function(value) {
+    if (isThenable(value)) {
+      var funcName =  (typeof value.done == "function") ? "done" : "then";
+      value[funcName](resolve, reject);
+      return;
+    }
+    accept(value);
+  };
+  var ifNotResolved = function(func) {
+    return function(value) {
+      if (!isResolved) {
+        isResolved = true;
+        func(value);
+      } else {
+        if (typeof console != "undefined") {
+          console.error("Cannot resolve a Future mutliple times.");
+        }
+      }
+    }
+  };
+
+  // Indirectly resolves the Future, chaining any passed Future's resolution
+  this.resolve = ifNotResolved(resolve);
+
+  // Directly accepts the future, no matter what value's type is
+  this.accept = ifNotResolved(accept);
+
+  // Rejects the future
+  this.reject = ifNotResolved(reject);
+
+  this.cancel  = function() { resolver.reject(new Error("Cancel")); };
+  this.timeout = function() { resolver.reject(new Error("Timeout")); };
+
+  if (underTest) {
+    Object.defineProperties(this, {
+      _isResolved: _readOnlyProperty(function() { return isResolved; }),
+    });
+  }
+
+  setState("pending");
+};
+
+//
+// Future Constuctor
+//
+
+var Future = function(init) {
+  var acceptCallbacks = new Backlog();
+  var rejectCallbacks = new Backlog();
+  var value;
+  var error;
+  var state = "pending";
+
+  if (underTest) {
+    Object.defineProperties(this, {
+      _value: _readOnlyProperty(function() { return value; }),
+      _error: _readOnlyProperty(function() { return error; }),
+      _state: _readOnlyProperty(function() { return state; }),
+    });
+  }
+
+  Object.defineProperties(this, {
+    _addAcceptCallback: _pseudoPrivate(
+      function(cb) {
+        acceptCallbacks.push(cb);
+        if (state == "accepted") {
+          acceptCallbacks.pump(value);
+        }
+      }
+    ),
+    _addRejectCallback: _pseudoPrivate(
+      function(cb) {
+        rejectCallbacks.push(cb);
+        if (state == "rejected") {
+          rejectCallbacks.pump(error);
+        }
+      }
+    ),
+  });
+  var r = new Resolver(this,
+                       acceptCallbacks, rejectCallbacks,
+                       function(v) { value = v; },
+                       function(e) { error = e; },
+                       function(s) { state = s; })
+  try {
+    if (init) { init(r); }
+  } catch(e) {
+    r.reject(e);
+  }
+};
+
+//
+// Consructor
+//
+
+var isCallback = function(any) {
+  return (typeof any == "function");
+};
+
+// Used in .then()
+var wrap = function(callback, resolver, disposition) {
+  if (!isCallback(callback)) {
+    // If we don't get a callback, we want to forward whatever resolution we get
+    return resolver[disposition].bind(resolver);
+  }
+
+  return function() {
+    try {
+      var r = callback.apply(null, arguments);
+      resolver.resolve(r);
+    } catch(e) {
+      // Exceptions reject the resolver
+      resolver.reject(e);
+    }
+  };
+};
+
+var addCallbacks = function(onaccept, onreject, scope) {
+  if (isCallback(onaccept)) {
+    scope._addAcceptCallback(onaccept);
+  }
+  if (isCallback(onreject)) {
+    scope._addRejectCallback(onreject);
+  }
+  return scope;
+};
+
+//
+// Prototype properties
+//
+
+Future.prototype = Object.create(null, {
+  "then": _public(function(onaccept, onreject) {
+    // The logic here is:
+    //    We return a new Future whose resolution merges with the return from
+    //    onaccept() or onerror(). If onaccept() returns a Future, we forward
+    //    the resolution of that future to the resolution of the returned
+    //    Future.
+    var f = this;
+    return new Future(function(r) {
+      addCallbacks(wrap(onaccept, r, "resolve"),
+                   wrap(onreject, r, "reject"), f);
+    });
+  }),
+  "done": _public(function(onaccept, onreject) {
+    return addCallbacks(onaccept, onreject, this);
+  }),
+  "catch": _public(function(onreject) {
+    return addCallbacks(null, onreject, this);
+  }),
+});
+
+//
+// Statics
+//
+
+Future.isThenable = isThenable;
+
+var toFuture = function(valueOrFuture) {
+  if (Future.isThenable(valueOrFuture)) {
+    return valueOrFuture;
+  } else {
+    return new Future(function(r) {
+      r.resolve(valueOrFuture);
+    });
+  }
+};
+
+var toFutureList = function(list) {
+  return Array.prototype.slice.call(list).map(toFuture);
+};
+
+/*
+Future.some = function() {
+  // TODO(slightlyoff)
+  var futures = toFutureList(arguments);
+};
+*/
+
+Future.any = function(/*...futuresOrValues*/) {
+  var futures = toFutureList(arguments);
+  return new Future(function(r) {
+    if (!futures.length) {
+      r.reject("No futures passed to Future.any()");
+    } else {
+      var count = 0;
+      var accumulateFailures = function(e) {
+        count++;
+        if (count == futures.length) {
+          r.reject();
+        }
+      };
+      futures.forEach(function(f, idx) {
+        f.done(r.resolve, accumulateFailures);
+      });
+    }
+  });
+};
+
+Future.every = function(/*...futuresOrValues*/) {
+  var futures = toFutureList(arguments);
+  return new Future(function(r) {
+    if (!futures.length) {
+      r.reject("No futures passed to Future.every()");
+    } else {
+      var values = new Array(futures.length);
+      var count = 0;
+      var accumulate = function(idx, v) {
+        count++;
+        values[idx] = v;
+        if (count == futures.length) {
+          r.resolve(values);
+        }
+      };
+      futures.forEach(function(f, idx) {
+        f.done(accumulate.bind(null, idx), r.reject);
+      });
+    }
+  });
+};
+
+//
+// Export
+//
+
+if(typeof module === 'object' && module.exports) {
+  module.exports = Future;
+}
+else {
+  global.Future = Future;
+}
+
+})(this,
+  (typeof window !== 'undefined') ? window : {},
+  this.runningUnderTest||false);
--- a/playground/index.html	Tue Apr 23 18:16:26 2013 +0200
+++ b/playground/index.html	Wed Apr 24 17:06:07 2013 -0400
@@ -17,6 +17,7 @@
       <script type="text/javascript" src="../common/prettify.js"></script>
       <script type="text/javascript" src="../common/lang-jsonld.js"></script>
       <script type="text/javascript" src="../common/lang-nquads.js"></script>
+      <script type="text/javascript" src="Future.js"></script>
       <script type="text/javascript" src="jsonld.js"></script>
       <script type="text/javascript" src="playground.js"></script>
       <script type="text/javascript" src="playground-examples.js"></script>
--- a/playground/jsonld.js	Tue Apr 23 18:16:26 2013 +0200
+++ b/playground/jsonld.js	Wed Apr 24 17:06:07 2013 -0400
@@ -69,14 +69,18 @@
   options = options || {};
 
   if(ctx === null) {
-    return callback(new JsonLdError(
+    return jsonld.nextTick(function() {
+      callback(new JsonLdError(
       'The compaction context must not be null.',
       'jsonld.CompactError'));
+    });
   }
 
   // nothing to compact
   if(input === null) {
-    return callback(null, null);
+    return jsonld.nextTick(function() {
+      callback(null, null);
+    });
   }
 
   // set default options
@@ -100,10 +104,12 @@
   }
 
   var expand = function(input, options, callback) {
-    if(options.skipExpansion) {
-      return callback(null, input);
-    }
-    jsonld.expand(input, options, callback);
+    jsonld.nextTick(function() {
+      if(options.skipExpansion) {
+        return callback(null, input);
+      }
+      jsonld.expand(input, options, callback);
+    });
   };
 
   // expand input then do compaction
@@ -235,36 +241,38 @@
     options.keepFreeFloatingNodes = false;
   }
 
-  // retrieve all @context URLs in the input
-  input = _clone(input);
-  _retrieveContextUrls(input, options, function(err, input) {
-    if(err) {
-      return callback(err);
-    }
-    try {
-      // do expansion
-      var activeCtx = _getInitialContext(options);
-      var expanded = new Processor().expand(
-        activeCtx, null, input, options, false);
-
-      // optimize away @graph with no other properties
-      if(_isObject(expanded) && ('@graph' in expanded) &&
-        Object.keys(expanded).length === 1) {
-        expanded = expanded['@graph'];
-      }
-      else if(expanded === null) {
-        expanded = [];
-      }
-
-      // normalize to an array
-      if(!_isArray(expanded)) {
-        expanded = [expanded];
-      }
-      callback(null, expanded);
-    }
-    catch(ex) {
-      callback(ex);
-    }
+  jsonld.nextTick(function() {
+    // retrieve all @context URLs in the input
+    input = _clone(input);
+    _retrieveContextUrls(input, options, function(err, input) {
+      if(err) {
+        return callback(err);
+      }
+      try {
+        // do expansion
+        var activeCtx = _getInitialContext(options);
+        var expanded = new Processor().expand(
+          activeCtx, null, input, options, false);
+
+        // optimize away @graph with no other properties
+        if(_isObject(expanded) && ('@graph' in expanded) &&
+          Object.keys(expanded).length === 1) {
+          expanded = expanded['@graph'];
+        }
+        else if(expanded === null) {
+          expanded = [];
+        }
+
+        // normalize to an array
+        if(!_isArray(expanded)) {
+          expanded = [expanded];
+        }
+        callback(null, expanded);
+      }
+      catch(ex) {
+        callback(ex);
+      }
+    });
   });
 };
 
@@ -619,21 +627,23 @@
     }
   }
 
-  // handle special format
-  if(options.format) {
-    // supported formats
-    if(options.format in _rdfParsers) {
-      dataset = _rdfParsers[options.format](dataset);
-    }
-    else {
-      throw new JsonLdError(
-        'Unknown input format.',
-        'jsonld.UnknownFormat', {format: options.format});
-    }
-  }
-
-  // convert from RDF
-  new Processor().fromRDF(dataset, options, callback);
+  jsonld.nextTick(function() {
+    // handle special format
+    if(options.format) {
+      // supported formats
+      if(options.format in _rdfParsers) {
+        dataset = _rdfParsers[options.format](dataset);
+      }
+      else {
+        throw new JsonLdError(
+          'Unknown input format.',
+          'jsonld.UnknownFormat', {format: options.format});
+      }
+    }
+
+    // convert from RDF
+    new Processor().fromRDF(dataset, options, callback);
+  });
 };
 
 /**
@@ -715,16 +725,100 @@
     'jsonld.ContextUrlError'), url);
 };
 
+/* Futures/Promises API */
+
+jsonld.futures = jsonld.promises = function() {
+  var Future = _nodejs ? require('./Future') : window.Future;
+
+  // converts a node.js async op into a future w/boxed resolved value(s)
+  function futurize(op) {
+    var args = Array.prototype.slice.call(arguments, 1);
+    return new Future(function(resolver) {
+      op.apply(null, args.concat(function(err, value) {
+        if(err) {
+          resolver.reject(err);
+        }
+        else {
+          resolver.resolve(value);
+        }
+      }));
+    });
+  }
+
+  // converts a load context promise callback to a node-style callback
+  function createContextLoader(promise) {
+    return function(url, callback) {
+      promise(url).then(
+        // success
+        function(remoteContext) {
+          callback(null, remoteContext.url, remoteContext.context);
+        },
+        // failure
+        callback
+      );
+    };
+  }
+
+  var api = {};
+  api.expand = function(input) {
+    var options = (arguments.length > 1) ? arguments[1] : {};
+    if('loadContext' in options) {
+      options.loadContext = createContextLoader(options.loadContext);
+    }
+    return futurize(jsonld.expand, input, options);
+  };
+  api.compact = function(input, ctx) {
+    var options = (arguments.length > 2) ? arguments[2] : {};
+    if('loadContext' in options) {
+      options.loadContext = createContextLoader(options.loadContext);
+    }
+    var compact = function(input, ctx, options, callback) {
+      // ensure only one value is returned in callback
+      jsonld.compact(input, ctx, options, function(err, compacted) {
+        callback(err, compacted);
+      });
+    };
+    return futurize(compact, input, ctx, options);
+  };
+  api.flatten = function(input, ctx) {
+    var options = (arguments.length > 2) ? arguments[2] : {};
+    if('loadContext' in options) {
+      options.loadContext = createContextLoader(options.loadContext);
+    }
+    return futurize(jsonld.flatten, input, ctx, options);
+  };
+  api.frame = function(input, frame) {
+    var options = (arguments.length > 2) ? arguments[2] : {};
+    if('loadContext' in options) {
+      options.loadContext = createContextLoader(options.loadContext);
+    }
+    return futurize(jsonld.frame, input, frame, options);
+  };
+  api.fromRDF = function(dataset) {
+    var options = (arguments.length > 1) ? arguments[1] : {};
+    return futurize(jsonld.fromRDF, dataset, options);
+  };
+  api.toRDF = function(input) {
+    var options = (arguments.length > 1) ? arguments[1] : {};
+    if('loadContext' in options) {
+      options.loadContext = createContextLoader(options.loadContext);
+    }
+    return futurize(jsonld.toRDF, input, options);
+  };
+  api.normalize = function(input) {
+    var options = (arguments.length > 1) ? arguments[1] : {};
+    if('loadContext' in options) {
+      options.loadContext = createContextLoader(options.loadContext);
+    }
+    return futurize(jsonld.normalize, input, options);
+  };
+  return api;
+};
+
 /* WebIDL API */
 
 function JsonLdProcessor() {};
-JsonLdProcessor.prototype.expand = jsonld.expand;
-JsonLdProcessor.prototype.compact = jsonld.compact;
-JsonLdProcessor.prototype.flatten = jsonld.flatten;
-JsonLdProcessor.prototype.frame = jsonld.frame;
-JsonLdProcessor.prototype.fromRDF = jsonld.fromRDF;
-JsonLdProcessor.prototype.toRDF = jsonld.toRDF;
-JsonLdProcessor.prototype.normalize = jsonld.normalize;
+JsonLdProcessor.prototype = jsonld.futures();
 JsonLdProcessor.prototype.toString = function() {
   return '[object JsonLdProcessor]';
 };
--- a/playground/playground.js	Tue Apr 23 18:16:26 2013 +0200
+++ b/playground/playground.js	Wed Apr 24 17:06:07 2013 -0400
@@ -190,80 +190,68 @@
   };
 
   /**
-   * Performs the JSON-LD API action based on the active tab.
+   * Returns a Future to performs the JSON-LD API action based on the active
+   * tab.
    *
    * @param input the JSON-LD object input or null no error.
    * @param param the JSON-LD param to use.
-   * @param callback(err) called once the operation completes.
    */
-  playground.performAction = function(input, param, callback) {
-    // set base IRI
-    var options = {base: document.baseURI};
+  playground.performAction = function(input, param) {
+    return new Future(function(resolver) {
+      var processor = new jsonld.JsonLdProcessor();
 
-    if(playground.activeTab === 'tab-compacted') {
-      jsonld.compact(input, param, options, function(err, compacted) {
-        if(err) {
-          return callback(err);
-        }
-        $('#compacted').html(js_beautify(
-          playground.htmlEscape(JSON.stringify(compacted)),
-          {'indent_size': 2}).replace(/\n/g, '<br>'));
-        callback();
-      });
-    }
-    else if(playground.activeTab === 'tab-expanded') {
-      jsonld.expand(input, options, function(err, expanded) {
-        if(err) {
-          return callback(err);
-        }
-        $('#expanded').html(js_beautify(
-          playground.htmlEscape(JSON.stringify(expanded)),
-          {'indent_size': 2}).replace(/\n/g, '<br>'));
-        callback();
-      });
-    }
-    else if(playground.activeTab === 'tab-flattened') {
-      jsonld.flatten(input, param, options, function(err, flattened) {
-        if(err) {
-          return callback(err);
-        }
-        $('#flattened').html(js_beautify(
-          playground.htmlEscape(JSON.stringify(flattened)),
-          {'indent_size': 2}).replace(/\n/g, '<br>'));
-        callback();
-      });
-    }
-    else if(playground.activeTab === 'tab-framed') {
-      jsonld.frame(input, param, options, function(err, framed) {
-        if(err) {
-          return callback(err);
-        }
-        $('#framed').html(js_beautify(
-          playground.htmlEscape(JSON.stringify(framed)),
-          {'indent_size': 2}).replace(/\n/g, '<br>'));
-        callback();
-      });
-    }
-    else if(playground.activeTab === 'tab-nquads') {
-      options.format = 'application/nquads';
-      jsonld.toRDF(input, options, function(err, nquads) {
-        if(err) {
-          return callback(err);
-        }
-        $('#nquads').html(playground.htmlEscape(nquads).replace(/\n/g, '<br>'));
-        callback();
-      });
-    }
-    else if(playground.activeTab === 'tab-normalized') {
-      options.format = 'application/nquads';
-      jsonld.normalize(input, options, function(err, normalized) {
-        if(err) {
-          return callback(err);
-        }
-        $('#normalized').html(playground.htmlEscape(normalized).replace(/\n/g, '<br>'));
-        callback();
-      });
-    }
+      // set base IRI
+      var options = {base: document.baseURI};
+
+      if(playground.activeTab === 'tab-compacted') {
+        processor.compact(input, param, options).done(function(compacted) {
+          $('#compacted').html(js_beautify(
+            playground.htmlEscape(JSON.stringify(compacted)),
+            {'indent_size': 2}).replace(/\n/g, '<br>'));
+          resolver.resolve();
+        }, resolver.reject);
+      }
+      else if(playground.activeTab === 'tab-expanded') {
+        processor.expand(input, options).done(function(expanded) {
+          $('#expanded').html(js_beautify(
+            playground.htmlEscape(JSON.stringify(expanded)),
+            {'indent_size': 2}).replace(/\n/g, '<br>'));
+          resolver.resolve();
+        }, resolver.reject);
+      }
+      else if(playground.activeTab === 'tab-flattened') {
+        processor.flatten(input, param, options).done(function(flattened) {
+          $('#flattened').html(js_beautify(
+            playground.htmlEscape(JSON.stringify(flattened)),
+            {'indent_size': 2}).replace(/\n/g, '<br>'));
+          resolver.resolve();
+        }, resolver.reject);
+      }
+      else if(playground.activeTab === 'tab-framed') {
+        processor.frame(input, param, options).done(function(framed) {
+          $('#framed').html(js_beautify(
+            playground.htmlEscape(JSON.stringify(framed)),
+            {'indent_size': 2}).replace(/\n/g, '<br>'));
+          resolver.resolve();
+        }, resolver.reject);
+      }
+      else if(playground.activeTab === 'tab-nquads') {
+        options.format = 'application/nquads';
+        processor.toRDF(input, options).done(function(nquads) {
+          $('#nquads').html(
+            playground.htmlEscape(nquads).replace(/\n/g, '<br>'));
+          resolver.resolve();
+        }, resolver.reject);
+      }
+      else if(playground.activeTab === 'tab-normalized') {
+        options.format = 'application/nquads';
+        processor.normalize(input, options).done(function(normalized) {
+          $('#normalized').html(
+            playground.htmlEscape(normalized).replace(/\n/g, '<br>'));
+          resolver.resolve();
+        }, resolver.reject);
+      }
+    });
   };
 
   /**
@@ -326,13 +314,7 @@
     }
 
     // no errors, perform the action and display the output
-    playground.performAction(input, param, function(err) {
-      if(err) {
-        // FIXME: add better error handling output
-        $('#processing-errors').text(JSON.stringify(err));
-        return;
-      }
-
+    playground.performAction(input, param).done(function() {
       // generate a link for current data
       var link = '?json-ld=' + encodeURIComponent(JSON.stringify(input));
       if($('#frame').val().length > 0) {
@@ -358,6 +340,9 @@
 
       // start the colorization delay
       playground.checkColorizeDelay(true);
+    }, function(err) {
+      // FIXME: add better error handling output
+      $('#processing-errors').text(JSON.stringify(err));
     });
   };
 
--- a/test-suite/idltest/index.html	Tue Apr 23 18:16:26 2013 +0200
+++ b/test-suite/idltest/index.html	Wed Apr 24 17:06:07 2013 -0400
@@ -5,12 +5,12 @@
 <title>JSON-LD idlharness test</title>
 <link rel="author" title="W3C" href="http://www.w3.org/" />
 <link rel="help" href="http://www.w3.org/TR/json-ld-api/#jsonldprocessor" />
-<link rel="help" href="http://json-ld.org/spec/latest/json-ld-api/#jsonldcallback" />
 <link rel="help" href="http://json-ld.org/spec/latest/json-ld-api/#loadcontextcallback" />
-<link rel="help" href="http://json-ld.org/spec/latest/json-ld-api/#contextloadedcallback" />
+<link rel="help" href="http://json-ld.org/spec/latest/json-ld-api/#remotecontext" />
 <link rel="help" href="http://json-ld.org/spec/latest/json-ld-api/#jsonldoptions" />
 <link rel="help" href="http://json-ld.org/spec/latest/json-ld-api/#jsonlderror" />
 <link rel="help" href="http://json-ld.org/spec/latest/json-ld-api/#jsonlderrorcode" />
+<script src="../../playground/Future.js"></script>
 <script src="../../playground/jsonld.js"></script>
 <script src="http://w3c-test.org/resources/testharness.js"></script>
 <script src="http://w3c-test.org/resources/testharnessreport.js"></script>
@@ -28,22 +28,20 @@
 <pre id="idl">
 [Constructor]
 interface JsonLdProcessor {
-    void compact (JsonLdInput input, JsonLdContext context, JsonLdOptions options, JsonLdCallback callback);
-    void compact (JsonLdInput input, JsonLdContext context, JsonLdCallback callback);
-    void expand (JsonLdInput input, JsonLdOptions options, JsonLdCallback callback);
-    void expand (JsonLdInput input, JsonLdCallback callback);
-    void flatten (JsonLdInput input, JsonLdContext? context, JsonLdOptions options, JsonLdCallback callback);
-    void flatten (JsonLdInput input, JsonLdContext? context, JsonLdCallback callback);
+    Future compact (JsonLdInput input, JsonLdContext context, optional JsonLdOptions options);
+    Future expand (JsonLdInput input, optional JsonLdOptions options);
+    Future flatten (JsonLdInput input, JsonLdContext? context, optional JsonLdOptions options);
 };
 
 typedef (object or object[] or DOMString) JsonLdInput;
 typedef (object or DOMString) JsonLdContext;
 
-callback JsonLdCallback = void (JsonLdError error, optional (object or object[]) document);
+callback LoadContextCallback = Future (DOMString url);
 
-callback LoadContextCallback = void (DOMString url, ContextLoadedCallback callback);
-
-callback ContextLoadedCallback = void (JsonLdError error, optional DOMString url, optional DOMString context);
+dictionary RemoteContext {
+  DOMString url;
+  DOMString context;
+};
 
 dictionary JsonLdOptions {
     DOMString              base;