Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancellation Token #8

Open
ForbesLindesay opened this issue May 11, 2013 · 2 comments
Open

Cancellation Token #8

ForbesLindesay opened this issue May 11, 2013 · 2 comments

Comments

@ForbesLindesay
Copy link
Member

This is an alternative idea based on a cancellation token. The aim with this was to produce a better idea from a "security" point of view that still had nice semantics for propagation etc.

The core idea is that you provide a CancellationToken which represents the ability to be notified of cancellation. You can only provide this when a promise is created, but if a promise wasn't provided with one when it was created, you can provide one later.

Terminology

  1. A CancellationToken is an object with methods for determining whether an operation should be cancelled
  2. Exactly one CancellationToken can be associated with a promise.
  3. Many CancellationTokens can be registered with a promise.

Requirements

The arguments of a then method look like .then(onFulfilled, onRejected, onProgress, CancellationToken). For this spec, we are interested in CancellationToken.

CancellationToken

A CancellationToken is an object with the following two methods:

  1. .isCancelled() must always return true or false
  2. onCancelled(cb) must call cb if Cancellation is triggered.

Direct Cancellation

If a promise has a CancellationToken associated with it (i.e. it was created via a call to .then that included a CancellationToken) then it is cancelled if and only if that CancellationToken is cancelled. If that CancellationToken is cancelled, it is immediately rejected.

Parent Cancellation

If a promise has no CancellationToken associated with it directly, it can be cancelled indirectly. All calls to .then that create a new promise must register that new promise's associated CancellationToken with the current promise.

This promise gets an internal CancellationToken associated with it that is cancelled whenever all registered CancellationTokens have been cancelled (providing at least one tick has gone past to allow for more tokens to be registered).

Child Cancellation

Any promise returned from a call to onFulfilled or onRejected should have the current promise's associated CancellationToken (even if it's internal via Parent Cancellation) registered with it via the call to .then necessary for assimilation.

@gaearon
Copy link

gaearon commented Nov 27, 2013

I don't know if this is of use to anybody here, but here's what we're using, for the lack of a better pattern. It's pretty much based on what CancellationTokenSource/CancellationToken dichotomy I've seen in .NET, but simplified. Of course, I have to call token.throwIfCanceled manually.

I would love to see first-class support for cancellation in a spec, and I enjoy the idea of passing around the token.

var source = new window.CancellationTokenSource(),
    token = source.getToken();

// Cancel in three seconds.
window.setTimeout(function() {
  source.cancel();
}, 3000);

doFirstAsyncOperation(token) // it's safe to pass token around
  .then(token.throwIfCanceled) // may get canceled here
  .then(doSecondAsyncOperation)
  .then(function () {
    doCostlySynchronousOperation();
    token.throwIfCanceled (); // or here
    doSecondCostlySynchronousOperation();
  })
  .catch(token.catchItself) // will not propagate
  .done();




(function () {

  'use strict';

  window.CancellationTokenSource = function () {
    this._token = new window.CancellationToken(this);
    this._canceled = false;
  };

  _.extend(window.CancellationTokenSource.prototype, {
    getToken: function () {
      return this._token;
    },

    isCanceled: function () {
      return this._canceled;
    },

    cancel: function () {
      if (this.isCanceled()) {
        throw new Error('This token source is already canceled.');
      }

      this._canceled = true;
    }
  });

})();

(function () {

  'use strict';

  window.CancellationToken = function (source) {
    this._source = source;
    _.bindAll(this, 'catchItself', 'throwIfCanceled');
  };

  _.extend(window.CancellationToken.prototype, {
    isCanceled: function () {
      return this._source.isCanceled();
    },

    throwIfCanceled: function (previousResult) {
      if (this.isCanceled()) {
        throw this;
      }

      return previousResult;
    },

    catchItself: function (ex) {
      if (ex !== this) {
        throw ex;
      }
    },

    toString: function () {
      return 'cancellation token';
    }
  });

  _.extend(window.CancellationToken, {
    getNone: function () {
      return (new window.CancellationTokenSource ()).getToken();
    }
  });

})();

@bergus
Copy link

bergus commented Jul 15, 2014

What's the point of this addendum?

(providing at least one tick has gone past to allow for more tokens to be registered)

It's seem like it would allow for indeterminacies

var a = fulfilledPromise.then(doSomethingSync1);
var b = a.then(doSomethingSnync2);
b.cancel();
// or instead (we don't know whether this will be executed before doSomethingSync2 anyway)
fulfilledPromise.then(b.cancel);

Are a and b cancelled now, or did doSomethingSync succeed and fulfill a before it could get asynchronously cancelled? I think cancellation should always be synchronous.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants