Futures (was: Request for JSON-LD API review)

Ron Buckton rbuckton at chronicles.org
Fri Apr 19 18:27:36 PDT 2013


From: Tab Atkins Jr. [mailto:jackalmage at gmail.com]
Sent: Friday, April 19, 2013 3:14 PM
> 
> On Fri, Apr 19, 2013 at 2:24 PM, Ron Buckton <rbuckton at chronicles.org>
> wrote:

> > Progress notifications are a bit of a mixed bag for me. My first
> > implementations of Future didn’t have them, but I had a number of
> > instances in the C# world where I needed a way to update the Future
> > consumer for the benefit of UI notifications. The recent discussion on
> > EventStreams has me wondering if progress notifications could serve a
> > similar purpose for Futures, where a progress notification could be
> > triggered for each instance of an event in the stream, where resolve
> > is triggered for single use events (like DOMContentLoaded), or when
> > the event producer is signaling that it has concluded processing.
> 
> I've also given thought to this, but, even though they're structurally similar at
> first glance, the use-cases for ProgressFuture and EventStream are actually
> quite different.  A ProgressFuture is still fundamentally focused on the
> fulfilled/rejected state, while an EventStream is fundamentally focused on
> updates.

I can agree, though there is some overlap in the capabilities they are definitely focused on solving different problems.
 
> > With Progress notifications relegated to a subclass, would chained
> > Futures also be ProgressFuture instances? The benefit of having
> > progress as an optional member of the Future class is that a chained
> > Future could also enlist in progress notifications, but that is less
> > of a concern if the Future created by a .then from a ProgressFuture is itself
> a ProgressFuture.
> 
> I'm currently of the opinion that progress updates should probably
> automatically bubble through chains.  (I'm currently doing the similar thing
> with EventStreams, and making completion bubble through.)	

I agree. Whether using a ProgressFuture or not, these kinds of notifications need to bubble down the chain of descendants until they reach an interested caller. The problem would be how to allow interested callers to be able to intercept that message without themselves being a ProgressFuture instance.  This leads me back to the point that Progress notifications make some sense on Future. Alternatively, the result of calling then or catch on a ProgressFuture should then return a new Progress future.  

Granted, I imagine *most* API's won't have need of the progress API. XHR has some limited use cases, though transmitting files using the File API, or a Future that sits atop a Worker or WebSocket could make use of progress in a meaningful fashion.

> 
> > Cancellation was an attempt to support something akin to Cooperative
> > Cancellation as it exists in .NET’s implementation, as well as a means
> > to ‘unlisten’ or forget a Future if you no longer need its value. In
> > the API proposal, by default cancel would only essentially remove a
> > Future (and therefore its chained descendants) from receiving
> > resolve/reject/progress signals from its antecedent. Cancellation also
> > would allow the ability to prevent the resolve/reject/progress
> > callbacks from executing in a later turn of the dispatcher to prevent
> > the execution of now unneeded code.  It can also be used to abort a XHR
> request or shut down a WebWorker.
> 
> The problem with cancellation, as stated, is that it allows one consumer to
> affect the state that another consumer sees.  Right now, that's not a
> possibility, which lets you reason about futures much more easily.  (The fact
> that you can do this in jQuery's promises, for example, makes them
> extremely hard to work with generically.)
> 
> As Alex says, creating a Future subclass that's single-listener would avoid this
> issue, so cancelling would probably work.

If you take out the optional "antecedent" argument in my gist, cancel here would be designed to remove the Future and only its chained descendants from receiving a resolve or reject signal from that future's antecedent.  There is the question of how to handle cancellation notifications as they bubble down the chain.  One option is to have cancellation merely reject the future with something like a "CancelledError". Descendants in the chain with a reject handler could inspect the argument to their handler to make a determination about what to do when it is cancelled. The downside is that this adds additional operations to the dispatcher to asynchronously handle the cancellation state. Ideally I would want to prevent all of these possible operations from completing and only have to deal with handlers for cancellation cleanup.

The cancelCallback argument proposed for Promise is designed to provide the means of interpreting that cancellation signal.  You might, for example, perform an XHR GET in the following fashion:

function fetchAsync(url) {
  var xhr = new XMLHttpRequest();
  return new Promise(function(resolver) {
    xhr.onload = function() { resolver.resolve(xhr.responseText); }
    xhr.onerror = function() { resolver.resolve(xhr.statusText); }
    xhr.open("GET", url, true);
    xhr.send();
  }, function () {
    xhr.abort();
  });
}

Unfortunately, it does make the function a bit odd with respect to cancellation, as I'm forced to lift the xhr reference out of the initCallback. In an earlier rev, I would have the user create the PromiseResolver (called PromiseSource at the time) first, and it had a .promise property that returned the Promise. The constructor to PromiseSource took in a cancellation callback. That looked more like the following:

function fetchAsync(url) {
  var xhr = new XMLHttpRequest();
  var source = new PromiseSource(function () { xhr.abort(); });
  xhr.onload = function() { source.resolve(xhr.responseText); }
  xhr.onerror = function() { source.reject(xhr.statusText); }
  xhr.open("GET", url, true);
  xhr.send();
  return source.promise;
}

> > The second callback in the Promise constructor would be a means to
> > provide user supplied cancellation logic, such a updating the UI in
> > response to a cancelled pending operation.  I debated on whether it
> > should be possible to also cancel the antecedent tasks from a chained
> > descendent, and it is a very tentative part of the API.
> 
> Most consumers of Futures won't be using the constructor - they'll just be
> handed an already-constructed future for them to listen to.
> So, using the constructor as the channel to pass in cancellation info won't
> really help. :/

In the .NET world, if you were passing along a CancellationToken you could use the Register method to queue up a callback to execute if cancellation was requested.  If we considered the cancellation-by-future approach, it might look something like this: https://gist.github.com/rbuckton/5424214 

In that way, we can simply use a Future for cancellation, and attach custom cleanup steps using Future#done.  There are a few caveat's with this approach. Since there are no properties on a Future to know if it's been cancelled, it's harder to cooperate with cancellation logic. Also, cancellation-by-future doesn't prevent chained descendants from possibly queuing tasks on the dispatcher to handle a possible reject signal, and doesn't remove pending tasks from the dispatcher that have not yet been processed, and at this point don't need to.

In building SPAs, I've found that it's often necessary to cancel an async operation. A user might make a request for a page of data that requires a server-side fetch, but then decide to switch to the next page or perform an operation that might require a server-side sort, etc. In those cases, the previously requested Futures that may have not yet completed are merely costing us additional turns or wasting network bandwidth. 

> > The reason options is expected to be an object/object literal is that
> > this can be extended to add additional control over the resulting
> continuation.
> > This could include the ability to prevent cancellation (in the event
> > .cancel is supported with the antecedents argument), or the ability to
> > only signal chained descendants if a future is rejected and not to
> > forward resolve to those descendants. This also allows for future
> > additions to the options in later versions without breaking consumers.
> > In this vein, it could be useful to have an options argument for the
> > Future constructor as well, although I haven’t yet had an occasion to need
> one yet.
> 
> Interesting ideas!

The options argument might also be a valid place to trap additional signals such as cancel, progress, etc. without polluting then/done. 

> > Finally, the additional API definitions are convenience APIs for
> > certain scenarios. By default, I expect both Promise.resolve and
> > PromiseResolver#resolve to only hook the resolve/reject of a Promise
> > from the same library. Calling Promise as a function (or adding a
> > Promise.of static method) might be the only Promise ‘interop' to
> > userland Future libraries, though I would almost prefer that no
> > ‘interop’ between libraries for a DOM or ES version to exist, but
> > rather would require explicitly creating a new Future and using its
> > resolver to interoperate with the userland promise.
> 
> Correct, and I expect the same.  (That said, we can probably at least adopt
> Promises/A+ adoption semantics, where thenables with behavior that is
> anywhere near sane can be automatically converted into
> Futures.)

As I understand it, that usually involves checking for a callable "then" data property on the result.  The drawback to that is that most Promise/A-like (or "thenable") implementations allocate a new Promise object for their "then" result, which we effectively ignore. Trapping a callable "done" would incur less overhead, but either way we fall into the trap of knowing the value is really a Promise/A-like through duck-typing.

> > The Promise.any, Promise.every, and Promise.some methods are very
> > similar to what is in DOMFutures, except that the current version of
> > the DOMFutures spec leaves a few things unspecified that could be
> > problematic for end users. According to the spec for Future.every,
> > order of the resolved values is arbitrary, based on the order that the
> > provided Futures are resolved. As a consumer of Future.every, the
> > Array of resolved values should be in the same order as the futures
> > that were provided to the method, to be able to distinguish which
> > value belongs to which future.  This may or may not be in the
> > polyfill, but it is not explicitly (or at least clearly) specified in
> > the DOMFutures spec. The same can be said for the Array of errors in the
> Future.some API definition.
> 
> Yes, the order of the result array does need to be in the same order as the
> input futures.  Good catch.  I'll file this in a new top-level thread.

I'm working on pushing up my working implementation to codeplex.com in the near future following these semantics. 

> > I added AggregateError as a tentative Error object as a means to
> > provide a single Error object to use as the value for the reject
> > handler, and have considered wrapping all non Error values passed to
> > the reject method on the resolver into an Error object to set
> > expectations for the consumer. That way, the argument to the reject
> > callback is always recognizable as an Error, and it can be easier to test the
> argument to provide appropriate handling.
> > For instance, without Error wrapping or AggregateError, I would have
> > to result to duck typing or Array.isArray to determine whether the
> > errors provided are the result of a single error or multiple errors
> > from a call to Future.some. This is, again, inspired by the .NET
> > AggregateException, though I would likely send the single underlying
> > Error if the AggregateError would only contain a single error.
> 
> No, Future.some always passes an array into the reject handler.  No need to
> duck-type, unless you're passing the same reject handler to multiple futures.
> If you are, Array.isArray() is reliable.

My main interest in having an Error (or AggregateError) is for the .stack property, assuming we can have a .stack that is meaningful when debugging Futures.

> > The remaining API’s are designed to help support await-style
> > asynchronous development as possibly afforded by generators or any
> > future addition of something like “await” into the language. To that
> > end, static methods like
> > Promise.yield() and Promise.sleep() can help to let the
> > dispatcher/event-loop do other work in the middle of a long-running
> > async function, or to pause for a period of time before continuing
> > such as with animation. Promise.delay() is similar to sleep, but resolves
> with a value.
> >
> > Promise.run() is close to setImmediate, where the result is the future
> > value of the callback. In this case, Promise#cancel() is then
> > effectively a call to clearImmediate. In a similar fashion,
> > Promise.start() is roughly equivalent to setTimeout with its
> > Promise#cancel() then synonymous with clearTimeout.
> 
> I expect these kind of conveniences to show up eventually, but likely in
> separate specs.  For example, Future.sleep() or Future.delay() would be
> defined alongside setTimeout().

Considering Future's already specifies in part how to handle queuing of tasks for the dispatcher/event-loop, it seems to make sense to have them as part of the Future spec. They're functionally similar to the Timers API, however their implementation might be different depending on the platform (e.g., use process.nextTick in Node.js). I am however basing this on the convenience methods of a similar nature that exist on Task in .NET.

Ron


More information about the es-discuss mailing list