-
Notifications
You must be signed in to change notification settings - Fork 5
Description
Update: A reworded and more detailed version can be found in this gist
I really love the idea of cancellation tokens from #8. However, I will go even further and amend/modify the specification for then, so that handlers themselves can be prevented from execution.
Terminology
- A
CancellationTokenis an object with methods for determining whether an operation can be cancelled. - One
CancellationTokenmight be associated with a promise. - Many
CancellationTokens can be registered with a promise. - A
CancellationErroris an error used to reject cancelled promises. - A cancelled promise is a promise that has been rejected with a
CancellationError. - A cancelled token is a
CancellationTokenthat is in the cancelled state, denoting that the result of the operation is no longer of interest. It might be considered an unregistered token or a revoked token. - A cancelled callback is an
onFulfilledoronRejectedargument to a.then()call whosecancellationTokenhas been revoked.
Requirements
The then method
Extensions are made to the following sections:
2.2.1. If onFulfilled is a function:
2.2.1.1. it must be called _unless it is cancelled_ after promise is fulfilled, with promise’s value as its first argument.
2.2.2.1. If onRejected is a function,
2.2.2.2. it must be called _unless it is cancelled_ after promise is rejected, with promise’s reason as its first argument.
Note: 2.2.1.3. and 2.2.2.3. ("must not be called more than once") stay in place, and still at most one of the two is called.
2.2.6.1. If/when promise is fulfilled, all respective _uncancelled_ onFulfilled callbacks must execute in the order of their originating calls to then.
2.2.6.2. If/when promise is rejected, all respective _uncancelled_ onRejected callbacks must execute in the order of their originating calls to then.
2.2.7.3. If onFulfilled is not a function and promise1 is fulfilled _and promise2 was not cancelled, promise2 must be fulfilled with the same value as promise1.
2.2.7.4. If onRejected is not a function and promise1 is rejected _and promise2 was not cancelled, promise2 must be rejected with the same reason as promise1.
(we probably need these last two in every cancellation spec anyway)
The CancellationToken
A CancellationToken is an object with a unique identity. It can get revoked, moving it into the cancelled state, which is an irreversible change.
The object has an isCancelled property, whose value must be a boolean[, or a function that returns a boolean]. It must yield true if the token is in the cancelled state, and false otherwise.
Retrieving the state of a cancellation token must not change the state, i.e. an isCancelled function must have no side effects.
The CancellationError
- It must be an instance of
Error(cancellationError instanceof Error === true). - It should have a
nameproperty with value"CancellationError". - It must have a
cancelledproperty with valuetrue.
The cancellationToken parameter
The fourth parameter of the then method is an optional cancellationToken; a call does look like
promise = parentPromise.then(onFulfilled, onRejected, onProgress, cancellationToken)
If cancellationToken is not a CancellationToken object, create an implicit CancellationToken for the new promise. In both cases (explicit and implicit) associate it with the new promise. The state of an explicit token must not be changed by the then method.
Register this cancellation token with the parentPromise.
Also register this cancellation token with any child promises that are returned from onFulfilled or onRejected (2.2.7.1). This includes passing the cancellation token to the then call in step 2.3.3.3. of the Promise Resolution Procedure.
If the promise is attempted to be cancelled with an error, run the following steps:
- If its associated token is an implicit token, test whether all the registered tokens on it are cancelled. If so, revoke the implicit token.
- If its associated token is not cancelled, return.
- If
parentPromiseis pending, attempt to cancel it witherror. - [If
onRejectedis a function and neither it noronFulfilledhave been called, execute it with theerroras its argument. (with 2.2.4. "async execution", and 2.2.7.1. "assimilation" in mind)] - If
onFulfilledoronRejectedhave been called and returned achildpromise, attempt to cancel that witherror. - [Only if none of the cancellation attempts was successfull [and
onRejectedwill not be executed]], rejectpromisewitherror. - Signal success.
The cancel method
The cancel method of a promise accepts two optional parameters:
promise.cancel(reason, token);
- Assert:
promiseis still pending. Returnfalseotherwise. - If
reasonis aCancellationError, leterrorbe that error object, else leterrorbe a newCancellationErrorwith thereasonas the value of itsmessageproperty. - If
tokenis aCancellationToken, revoke it. - Attempt to cancel the
promisewitherror.
The Promise constructor
Promises not created by a call to then may handle attempts to cancel them in implementation-dependent ways.
Constructors are however encouraged to signal these to the promise creators, and optionally provide them access to the list of registered tokens. This might be done through a callback that is passed as an additional argument to the Promise constructor, or returned from the resolver call.
Pluses:
-
no ambiguity when a promise is cancelled before the handlers of its resolved parent could be executed
// Example: var promise = fulfilled.then(willNeverBeExecuted); promise.cancel(); // or: parent.then(function() { promise.cancel() }); promise = parent.then(willNeverBeExecuted); -
making a promise uncancellable is trivial:
.then(null, null, null, {isCancelled:false}) -
forking a promise (to prevent immediate cancellation) is even more trivial:
.then()
Minus:
- I can't think of a way to implement
finallyin terms of.then()without preventing cancellation - it is not possible to add a handler via calling.then()without registering a token.
The basic idea of this draft is that handlers that were passed to then will not be executed when the cancellation token that accompanied them is cancelled:
var ajax = http.get(…);
ajax.then(doSomething);
var json = ajax.then(JSON.parse);
// later:
json.cancel(); // `ajax` can't be cancelled (because `doSomething` is still in-
// terested in it), but the `JSON.parse` won't need to be executed
I'm not so sure about the steps 4 and 6 of cancellation attempts.
- Putting step 4 into place ensures that still one of the two handlers is executed, and might allow interesting patterns to deal with cancellations.
- Putting the condition in step 6 allows for the cancellation error to bubble through the chain (without disturbing side
effectshandlers, notice that all forks/branches have beencutcancelled so it's a linear chain). For discussion, see issue Cancel every promise in the chain, or only the deepest cancellable ancestor? #10.