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

Tab Atkins Jr. jackalmage at gmail.com
Fri Apr 19 17:17:55 PDT 2013


On Fri, Apr 19, 2013 at 4:02 PM, Kevin Gadd <kevin.gadd at gmail.com> wrote:
> I'm not sure there's a perfect solution, yeah. Cancellation is definitely
> not something you want every listener to be responsible for in a
> multiple-listener scenario - most scenarios I've dealt with are ones where a
> single task is responsible for the lifetime of a future - deciding whether
> to cancel it, etc - usually the task that started it, but other tasks may be
> monitoring its progress. For example, a simple 'memoization' primitive might
> subscribe to a future in order to store its result when it is completed, in
> order to return a cached result the next time. The memoization primitive
> would never have a reason to cancel the future - that would be up to the
> task that actually requested the work. So it's tricky.
>
> .NET's standard library uses the 'cancellation token' primitive that Ron
> described, and I feel that's a pretty low-risk way to encapsulate
> cancellation, but it loses the benefits of having cancellation baked into
> the future itself - when I cancel a task via a cancellationtoken, for any
> subscribers to know about cancellation, I'll have to complete the Future
> (with some sort of special TaskCancelledError instead of a result?) or drop
> it on the floor and never complete it. So it creates a need for side-channel
> communication in all cancellation scenarios, and it requires all consumers
> to know whether or not a given Future can be cancelled. Maybe this is
> unavoidable.

Right, I think there are only two basic approaches that work:

1. Have a Future subclass that allows downstream consumers to affects its value.
2. Have a Future subclass that can only have one consumer, so the
consumer/producer distinction is safe to blur.

There's nothing intrinsically wrong with #1 - it's already the case
that XHR can do so, for example.  The problem is figuring out the best
way for other consumers to respond.

One simple possibility would be to just expose accept/resolve/reject
on the returned Future itself.  Calling any of these cancels the
Future (if the Future has a notion of cancellation), and forces it to
adopt the passed state as appropriate.  The constructor would take two
callbacks, one for normal operation (called immediately) and one to
handle cancellation (called when needed).  This has the nice benefit
that a consumer can provide a default value for other consumers to
use, and it doesn't require any new codeflow channels.

Another possibility is to add a cancel method on the returned Future,
and also expose a cancel listener, akin to the progress listener.
This exposes a new codeflow, which has to be handled (unsure whether
it should be a real codeflow channel, automatically passing down the
chain if unhandled, or just considered a subset of the rejection
channel, auto-rejecting the output promise if it's unhandled).

While #2 is probably appropriate for some cases, I think it's less
general, and providing both might be confusing.  (Not to mention the
confusion of having a brand new behavior for observing.)

> The split between functions that affect a Future and functions that consume
> it is definitely an interesting one. To be honest, my API never made the
> distinction - a Future is always read/write, and the state change model
> generally ensures that if the Future is mishandled, an exception will be
> thrown somewhere to notify you that you screwed up. But I think that
> capability split is probably important, and I don't know how cancellation
> fits into that model - in particular since ES6/ES7 seem very focused on
> using object capability as a security model, you don't want passing a Future
> across a boundary to give some third party the ability to fake the result of
> a network request or something like that.

Yes, the capability split is very important to allow reasoning about
it sanely, for the reason you give.  Maintaining this principle
suggests the proper way forward pretty clearly, I think.

If you pass something across a security boundary, and you're afraid of
them being able to fake it, you're afraid of them doing *anything* to
it.  This suggests that you actually want something like Q's Deferred,
which is basically a naked resolver object that you can pull a promise
off of.

It would be so nice if JS had multiple return values, so we could let
cancellable future-returning APIs just return a naked resolver as
their second value, and only clueful call sites would need to care
about it.  ^_^  Instead, we'll probably need to have API variants that
instead return something like a Deferred, or that return a pair of a
future and a resolver.

~TJ


More information about the es-discuss mailing list