tc39 / proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Other
375 stars 29 forks source link

Motivate CancelToken.any #47

Closed domenic closed 8 years ago

domenic commented 8 years ago

I forgot what the use cases for it were :(. @dtribble @benjamingr could you help with some C#-derived use cases? @stefanpenner do you remember what the use case was that we spontaneously came up with at TC39?

stefanpenner commented 8 years ago

Was any basically something like?

const { token, cancel } = Token.any([one, two, three]); // token cancels if any `one` `two` `three` or itself are cancelled

If so, I believe I can share examples.

domenic commented 8 years ago

Just token = instead of { token, cancel } =, but yeah.

bergus commented 8 years ago

An example I used in my draft is your last library with cancellation. With CancelToken.race, it would look like this:

function last(operation) {
    var s = CancelToken.source();
    return function(...args) {
        s.cancel("next operation already started"); // cancel previous token
        const token = args.pop();
        if (token.requested) return Promise.reject(Cancel.getReason(token)); // this is weird
        s = CancelToken.source(); // reinitialise source for next call
        const any = CancelToken.race([token, s.token]); // wait for current token or next call 
        return operation(...args, any); // pass token into operation
    };
}
benjamingr commented 8 years ago

I've never needed this in all our C# code, sorry. I did register the cancellation of one token to several tokens a few times - but being able to pass a CancellationTokenSource.Cancel to Register of another token has generally been sufficient.

domenic commented 8 years ago

Thanks @bergus; I added a version of that. I'd like to get a couple more examples before closing this.

bergus commented 8 years ago

Oh, and of course the trivial example can be a race (which cannot do autocancellation for the loosers by itself, when using with tokens like in this proposal):

function example(token) {
    const win = CancelToken.source();
    const combined = CancelToken.any([token, win.token]);
    return Promise.race([doA(combined), doB(combined)])
    .finally(() => win.cancel("already settled"));
}
benjamingr commented 8 years ago

I'm not sure race motivates it @bergus :

function example(token) {
    const win = CancelToken.source();
    token.promise.then(t => win.cancel(t));
    return Promise.race([doA(win), doB(win)])
    .finally(() => win.cancel("already settled"));
}
bergus commented 8 years ago

@benjamingr Right, what is currently specced isn't really motivated at all as not being easily achievable without a builtin. It might be a different story if the .requested property would immediately reflect the cancellation decision, see #45. I would favour userland implementations over limited builtin ones, at least until we see which ones are really needed. The cancellation mechanisms should respect a custom .requested property for that however. And also, all of the above examples have only combined two tokens not arbitrarily many, so it might be a better idea to use tokenA.or(tokenB) or tokenA.concat(tokenB) instead of any([tokenA, tokenB]).

ljharb commented 8 years ago

I don't see the benefit of hamstringing it to only being useful for two items, when N allows for 2 just fine.

bergus commented 8 years ago

Well, I'd just leave figuring out the most useful functionality to userland implementations. And even if there was only a binary function, you always could do any = tokens => tokens.reduce(or) :-)

stefanpenner commented 8 years ago

@domenic I believe one of the examples we discussed was more or less as follows. I left it very verbose (minimized on abstractions) so I don't distract others, but could expand on more concise API that might make this nice?

The general concept is:

Tokens at play in this example are::

  1. A token to represent the UI Component active isRendered
  2. A token to represent the animation isAnimating

The relationship between these tokens is:

isAnimating depends on isRendered, but isRendered does not depend on isAnimating.


A concrete example (UI Animations):

class UIComponent {
  constructor() {
    // token to represent the the upper bounds on the UIComponent being alive.
    this.isRendered = new Token(cancel => this._wasRemoved = cancel);
  }

  // triggered via by a UI event, or on initial render.
  startAnimation() {
     this.stopAnimation();
     // create new token for said animation
     let animationToken = new Token(cancel => this._stopAnimation = cancel);

     // animate until:
     //  * the animation is cancelled
     //  * the UI component is removed
     this.animate(Token.any([
       animationToken,
       this.isRendered
     ])
  }

  // triggered via by a UI event
  stopAnimation() {
     if (this._stopAnimation) { this._stopAnimation(); }
   }

  // UI Element was added to the UI
  didInsert() {
     // automatically start animation when the UI Component is inserted
     this.startAnimation();
  }

  // UI Element was removed from the UI
  willRemove() {
    this._wasRemoved();
  }

  // animation loop
  animate(until) {
    if (until.isCancelled) { return; }

    // some amazing animation:
    requestAnimationFrame(this.animate.bind(this, until));
  }
}

note: I have many more examples, and could extract use-cases from real world code if anyone believes that is of value


Some thoughts as I wrote this

This example reminds me why I believe let { token, cancel } = Token.any([a,b,]) may be valuable.

In the above example (and many more I can think of) the following:

let animationToken = new Token(cancel => this._stopAnimation = cancel);

// animate until:
//  * the animation is cancelled
//  * the UI component is removed
this.animate(Token.any([
  animationToken,
  this.isRendered
]);

can be more succinctly represented as:

let animationToken = new Token(cancel => this._stopAnimation = cancel);
let { token, cancel } = Token.any([this.isRendered, ...otherDependentTokens])
this._stopAnimation = cancel;

this.animate(token);

Another spin, or more of a mutation of @bergus example could result in something like:

let { token, cancel } = this.isRendered.or(...otherDependentTokens)
this._stopAnimation = cancel;
this.animate(token);