Kevin Gadd (2013-04-19T23:02:41.000Z)
github at esdiscuss.org (2013-07-12T02:26:58.887Z)
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. My particular API approach was simple, albeit not ideal: Since .NET has a 'Disposable' concept, my Future class simply became Disposable. So this meant that in all use cases, the simplest way to get cancellation 'right' was to use the language built-in: ```js var taskFuture = StartSomeTask(); // returns Future using (taskFuture) { // when this block is left, taskFuture is disposed // ... do some async work using taskFuture ... yield return taskFuture; // wait on taskFuture } ``` In practice what this meant is that when the task was suspended to wait on taskFuture, it currently 'owned' the lifetime of that future. As a result, if the *task itself* were cancelled, the task scheduler would dispose the task, and because the task currently owned the lifetime of taskFuture, disposing the task disposed taskFuture. Cancelling an already-complete future in this manner is totally safe, so the 'using' pattern ends up not having any downsides there - my Future implementation is basically first-come-first-serve, where if someone stores a result into a Future before you, they win (and you get an exception for trying to complete it twice), and if you cancel after a result has been stored into it the cancel is a no-op. If you wanted to go further with the design of a task scheduler you could automatically cancel any futures a task is waiting on, but I decided not to do that since I didn't have an opportunity to think through all the consequences. Essentially in my model, the v1.0 equivalent had tri-state Futures: Incomplete, CompletedWithResult, and CompletedWithError. Cancellation was introduced in a later rev of the API and added a fourth 'Disposed' state. >From a callback perspective I ended up with two callbacks, one for 'completion' (either with result or error - the premise being that if you handle one you always want to handle the other) and another for cancellation. 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.