Cancel Promise pattern (no cancellable promises)

Igor Baklan io.baklan at gmail.com
Sat Jan 7 13:02:16 UTC 2017


In general I thing it would be good to have something like
[``java``(``Thread.interrupt()``)](
https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#interrupt()),
assuming that "Thread" here can be "async stacktrace of promises", and
interrupt notification should be just forwarded to top entry
([promise-executor](
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise#Parameters))
and handled there arbitrary. In meta code it can be approximately expressed
like  ``promise.interrupt(interruption_config)`` ==>
``promise.topPromiseInAwaitChain().injectInterruptionNotification(interruption_config)``.
So it should be up to [promise-executor](
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise#Parameters)
- whether it want to complete abnormally on interruption (either with
success or with failure), or just ignore this signal and continue execution
without any reaction. It just assumes that general ``.then(...)``-like
promises or ``async (...)  => {...}``-like promises should propagate
interruption-signal transparently upward over async-stacktrace, and
interruption-signal would be handled only in promises with executors which
aware of interruption capability. In code it may look like

```js
const waitAsync = (delay) => (new Promise(
  (resOk, resErr, interuptionSignalEmitter) => {
    const beforeComplete = () => {clearTimeout(tid);}
    const tid = setTimeout(
      () => {beforeComplete(); resOk();},
      delay
    );
    interuptionSignalEmitter.on(
      (interuptionConfig) => {
        beforeComplete();
        if (interuptionConfig && interuptionConfig.raiseError) {
          resErr(new Error("abnormal interuption"));
        } else {
          resOk();
        }
      }
    );
  }
));

// waitAsync(100).then(() => {console.log("wait ok")}).interrupt() - should
complete successfully
//   ("wait ok" message should be printed) but without delay in 100ms
// waitAsync(100).then(() => {console.log("wait
ok")}).interrupt({raiseError: true}) - should complete with failure
//   (NO "wait ok" message should be printed) and without delay in 100ms
```


So, in other words, I would rather say, that we lack something like event
capturing pahase when we intent for abnormal-completion/cancellation. I
mean, if we have for example some async-stacktrace and some particular
entry in it in the middle (some running and not yet completed promise),
then it looks very natural that we may wish to "send a signal/message"
downward over async-stacktrace, it just can be made by throwing something
in that entry (and that "thrown something" will be naturally propagated
downward async-stacktrace/promises-chain). But in certain cases we may need
to "send a signal/message" upward over async-stacktrace which should
generally end up by handling in very top promise executor (and if that
top-most promise in chain decide to complete execution abnormally, then all
clean-up in promises-chain also happens "abnormally"), while if we just
"unsubscribe" some middle entry form it's "natural parent" and "abnormally"
assign some result to that entry, then ofcourse, all cleanup in "upper
part" of async-stacktrace will happen later (which ofcourse also can be
desired behavior in some cases).

Jan-Ivar Bruaroey wrote:
> Because it doesn't make fetch stop fetching, which is what people want
> as I understand it (to not have the fetch keep going even though they've
stopped waiting for it).

Completely agree, but I would rather prefer

```js
fetch(someUrl, someConfig).interuptOn(interruptionToken)
```
vs
```js
fetch(someUrl, {...someConfig, cancel: interruptionToken)
```
in this case ``.interruptOn`` can be easily defined on top of
``.interrupt`` like ``promise.interruptOn(interruptionReasonToken)`` <==>
``interruptionReasonToken.then((reason) => {promise.interrupt(reason)}),
promise``


Bergi wrote:
> Yes, that's what I mean. Sure, I could use `Promise.race` to get the
> cancellation even if the non-cancellable operation resumes, but that's
> quite ugly:
>
> Promise.race([
>      promise()
>      …chain…,
>      cancelToken
> ]).then(callback);
>
> especially when you'll need to nest that pattern. Instead, I'd just like
> to write
>
> promise()
> …chain…
> .then(callback, cancelToken)
>
> with the same behaviour.

Very reasonable. left-to-right ``.`` chaining is much more readable and
concise then wrap-like expression chaining.
But I would rather put ``cancelToken`` somewhere else, not in ``.then``
call.
>From my perspective it can look like

```js
Promise.race([
     promise()
     …chain…,
     cancelToken
]).then(callback);
```
<==>
```js
promise() …chain… cancellable().cancelOn(cancelToken).then(callback);
```
Where ``Promise::cancellable`` (``Promise.prototype.cancellable``) can
return some sort of "wrapper" - ``CancellablePromise(targetPromise)``
which in turns can be implemented based on ``Promise.race([targetPromise,
CancellablePromise::privateInternalCancelToken])`` and that
``privateInternalCancelToken`` can be fully controlled by
``CancellablePromise`` itself and completed(fulfilled/rejected) when ever
it needed (whether by direct call to
``cancellablePromise.completeNow(...)`` or caused by completion of some
promise passed to ``.cancelOn`` method)

And finally

Bergi wrote:
> I'd however love to be able to cancel specific chaining operations,
> i.e. `then` callbacks.

As for me, definitely such kind of possibility should be available. However
we can not unsubscribe ``then``-callback same easily as regular event
handler. Since at least [some sort of ``finally``-callbacks](
https://www.npmjs.com/package/promise.prototype.finally) should always work
on any item in remaining promises chain. So promise then-unsubscription is
tightly bound to providing immediate completion result for that target
promise (which would be used instead of callback invocation result). And
still it can be accomplished by some kind of third-party solutions, (like
in  snippets above) like ``promise.cancellable().then(() =>
{doSomth()}).completeImmediately({error:new Error("unsubscribed
abnormally")})``. If ``.cancellable()`` returns some wrapped instance like
``CancellablePromise(targetPromise)`` then ofcourse ``CancellablePromise``
can re-implement ``.then``, ``.catch``, etc by wrapping passed functions
and by wrapping ``targetPromise.then(...wrappedCallbacks)`` with
appropriate ``Promise.race([...])`` like
``Promise.race([targetPromise.then(...wrappedCallbacks),
internalCancellationToken])``, and then when ``.completeImmediately``
invoked coordinate appropriately wrapped callbacks and
``internalCancellationToken`` to prevent initial callbacks from being
executed and to complete "overall resulting promise" by the means of
``internalCancellationToken``.
But. I think disregarding that all of this stuff can be (more or less)
implemented in 3-rd parity libraries, it would be much better if it was
supported natively.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20170107/0debbe36/attachment-0001.html>


More information about the es-discuss mailing list