
Show HN: Cancelable async primitives for JavaScript - Mitranim
https://github.com/Mitranim/posterus
======
littlecranky67
Am I the only one that wonders why there is no reference/comparison to
observables such as provided by i.e. the RxJS library? They are async,
composable and cancelable and also widely used (see Angular >= 2).

~~~
Mitranim
Good suggestion, should probably address this in the readme.

Short answer: observables do too much, and Rx is WAY, WAAAAY too large. We
need simple async primitives before layering bigger abstractions on them. They
shouldn't take encyclopedic amount of reading to learn. They need to come in a
library that doesn't weigh over 100 KB.

~~~
antjanus
What about xstream?
[https://github.com/staltz/xstream](https://github.com/staltz/xstream) it's a
lightweigth alternative with a fraction of Rx's operators and a fraction of
its weight.

~~~
Mitranim
I guess this needs a bigger answer.

A well-designed tool should minimise the amount of states the program can be
in. Asynchronous programming is already horrifically bad, it generates
combinatorial explosions of intermediary states. Adding imperative, mutative
programming and an event-driven API only exacerbates the problem, increasing
the amount of possible state sequences even further. I don't think xtream
provides a good API.

------
mpodlasin
Just to point out - RxJs 5 allows you to bundle only operators you need, thus
significantly lowering output file size. You could actually ship to the
browser only features equivalent to those of this library. Not sure what
actual numbers are though.

~~~
Mitranim
This is news to me. Last time I looked into RxJS, I was unable to get a
minimum useful core of less than 100 KB minified. Might want to look again,
thanks!

~~~
littlecranky67
import { Observable } from "rxjs/Observable";

import "rxjs/add/operator/map"; import "rxjs/add/operator/reduce"; import
"rxjs/add/operator/take"; import "rxjs/add/observable/of";

// etc. - this way you only get what you need into your bundle

------
wging
Very interesting. Cancellation semantics of promises have bugged me for a
while. Looks promising (no pun intended).

If can make a suggestion--a cookbook of promise interop might be worth adding.
There's a fundamental incompatibility, given the cancellation concerns you
mention, so I'm sure it couldn't be 100% caveat-free, but it would still ease
adoption given the ecosystem's current standardization on promises.

~~~
Mitranim
Thanks for the suggestion. I tend to run everything on Futures, rarely dealing
with callbacks or promises, so interop tends to be a non-issue. But it's
probably worth including a promise-to-future convertion function.

What do you think is worth adding or writing about?

------
gyrgtyn
Can you compare/contrast with Fluture? [https://github.com/fluture-
js/Fluture](https://github.com/fluture-js/Fluture)

~~~
Mitranim
Thanks for the suggestion. I wasn't aware of Fluture. Will look deeper into
it.

On cursory inspection, looks like Fluture has multiple features that I
rejected when designing Posterus: laziness/templating, separation of
map/flatmap, and possibly others. They come at the cost of cognitive load and
API surface.

In contrast, Posterus aims to be the simplest Promise replacement you could
possibly come up with, filling the main missing features: cancelation and
scheduling control.

Additional features come at a cost. Laziness & templating trips up unfamiliar
developers. Separating map and flatmap doesn't make sense in a dynamic
language; since you can't statically enforce it, it's just a tripmine with no
benefit. Also, a large amount of utilities bloats the code size, which I care
about very much. Posterus is designed to be as small as a typical promise
polyfill, fitting into a size-constrained browser application.

Needless to say, Fluture has features you might want that don't exist in
Posterus. I'm not familiar with it, so it's difficult to recomment any.

------
inglor
You can also use cancellation tokens with promises. Pretty easy to do and
doesn't require a library.

~~~
Rapzid
There is a cancellation library kicking around npm that reasonably mimics
C#'s.

There must have been man-years of discussion on the Bluebird project around
cancellation, but for me it all comes back to tokens as every promise baked-in
solution feels a bit off. Golang effectively has them as well via the context
package.

*As you say it is pretty easy to do, but it's nice to standardize on an implementation

~~~
inglor
Yes, we've been reading all the golang discussion very carefully. It's nice to
see them go through the same mental process we went through - and my hopes are
that if we don't interfere they'll come up with a better idea we can just
copy.

We've had 4 cancellation proposals rejected so far (cancellation as rejection,
third state, cancellable-promise and cancel-tokens).

They work in bluebird pretty well - but people have concerns.

Personally I use our cancellation if I already have bluebird, and tokens if I
don't.

------
sampl
Might be a naive question, but how does this compare to existing Promise
libraries that have cancellation (like bluebird)?

[http://bluebirdjs.com/docs/api/cancellation.html](http://bluebirdjs.com/docs/api/cancellation.html)

~~~
photonios

      Here's an example: in Bluebird, cancelation doesn't
      propagate upstream. After registering onCancel in a
      promise constructor, you have to call .cancel() on
      that exact promise object. Calling .cancel() in any
      child promise created with .then() or .catch() will
      not abort the work, rendering the feature useless for
      the most common use case!
    
      True cancelation must propagate upstream, prevent
      all pending work, and immediately free resources and memory.
    

From the README

~~~
code-e
But the linked bluebird docs says:

    
    
      As an optimization, the cancellation signal propagates 
      upwards the promise chain so that an ongoing operation e.g. 
      network request can be aborted.

~~~
Mitranim
Interesting! This must be new. Bluebird didn't have upstream cancelation last
time I checked. Now it might actually be viable. Should update the readme.
Thanks!

Unfortunately it's still not viable in the browser due to its size, promises
too easily get converted into non-cancelables, and worst of all, async/await
forces native promises. If you want cancelable coroutines, you're forced to
roll a generator-based implementation such as what Posterus provides.

------
jzig
I can't think of any reason I've ever wanted to cancel a promise except maybe
when uploading a file. Therefore I'm having a hard time grasping why this is
such a big deal. What is another example?

~~~
Mitranim
Here's some examples from my experience.

* Server. Request handler starts expensive work. Let's say 4-5 database requests, some FS operations, rendering and sending response. Client disconnects. Running those operations will waste resources, we should stop. [1]

* Web. You can make at most 6 concurrent HTTP requests. They're precious resources. Dropping a request you no longer need will let others complete. It's nice to be able to abstract a painful ajax API behind something like a promise that doesn't lose the important ability to abort.

* React. View instance starts async fetch that eventually updates its state. User navigates to another view before it finishes. Updating the state after the component is unmounted is an error, and React will rightly complain. We should stop it.

* Server: abstracting an operation that's already cancelable behind a future. Let's say using Electron to render a website into a PDF. It's a really expensive operation, and the API is a pain to program against. You want to provide something as simple as a promise, but that doesn't lose the ability to stop it. [2]

[1] I write servers using Posterus coroutines, so all async code is
automatically owned and canceled where appropriate:
[https://github.com/Mitranim/koa-ring](https://github.com/Mitranim/koa-ring)

[2] This relies on futures to abstract away tricky timing management:
[https://github.com/Mitranim/epdf/blob/1c54481d4760a7eb730eb3...](https://github.com/Mitranim/epdf/blob/1c54481d4760a7eb730eb3dbe2e861abb26b68a9/lib/epdf.js#L37)

~~~
antjanus
This is great!

I actually had to write my way around non-cancellable promises quite a few
times and my situation mirrors yours though mostly on the client:

I have a search page that updates results every time a new filter is updated
(imagine you select a new tag to filter by, or narrow down the search
somehow). This can be done faster than responses come back and can create a
huge mess because some responses can come back faster than others.

This creates uncertainty in terms of what should be on the page. A few lines
of code and it's fixed but the cool thing is that I get to throw away results
that I don't care about and not process them (expensive-ish action).

This kind of situation happens somewhat often because a user can quickly
navigate around, fire off a ton of requests and only really cares about the
last one in the queue.

I haven't worked my way around it on a global scale which means that there
could be a ton of requests being processed and thrown away right after for no
good reason.

~~~
Mitranim
Glad you understand the problem. This is what futures are good for. Write an
XMLHttpRequest adapter that returns a future, have an easy time composing or
aborting operations.

------
calafrax
this is interesting work but at least on the node side I am going 100% native
async/await promises after node v8.3.0 is released. whatever small advantages
more advanced libraries may offer are for me overwhelmed by writing standard
spec compliant code now that the spec and implementation are finally (almost)
up to par.

~~~
abritinthebay
Seconded. I get the desire to free up the resources but unless you’re running
some extremely hot resource paths (huge db queries, etc) the need for this is
minimal.

Useful, yes. Absolutely. But I’ll wait on a standard spec first.

I’ve yet to see a clean implementation of this (bluebird comes closest
though).

~~~
Mitranim
Depends on use case. Sometimes you're surprisingly resource constrained.

People gave a few examples in this subtopic:
[https://news.ycombinator.com/item?id=14962684](https://news.ycombinator.com/item?id=14962684)

------
tannhaeuser
I'm admiring this work, but can't help to be increasingly concerned about the
utter complexity web developing is heading for. Case in point on a reddit
frequented by junior web devs (and product placement bots as it seems):
[https://www.reddit.com/r/webdev/comments/6sdglh/feeling_over...](https://www.reddit.com/r/webdev/comments/6sdglh/feeling_overwhelmed_with_the_current_web/)

~~~
ivan_gammel
User interfaces are hard to implement and JS is only approaching the
complexity of requirements for modern UI. It's no more a single form or a
button, so it should not be a surprise.

On the other side, JS indeed is ugly language with trash ecosystem (btw, how
long it will take for community to get rid of fsevents warning on non-Mac
systems?), but there are patterns forming and best practices being documented.
It can be learned, but juniors should not expect learning a huge engineering
discipline in a week.

~~~
coldtea
> _User interfaces are hard to implement_

They weren't that hard in Visual Basic 20 years ago, nor in any modern
environment like Cocoa -- and they didn't need all the craziness and frantic
over-engineering that goes on in JS frameworks.

The problem is everybody in the web rebuilds the whole UI from low level
primitives. Need a wizard? Make one yourself. Need a form? Build it. No
upfront structure to anything -- and the frameworks don't add enough either.

~~~
ivan_gammel
User interfaces are not just forms and wizards. You cannot build everything
from a high level template. That's why the whole UX discipline exists.

~~~
coldtea
> _User interfaces are not just forms and wizards._

Never said they were. Just used them as two basic examples, that web UIs still
overcomplicate and have problems with.

> _You cannot build everything from a high level template. That 's why the
> whole UX discipline exists._

The UX discipline exists for a totally orthogonal reason: to study,
understand, and suggest improvements to the design of interfaces (and the
resulting "user experience" with them), whether they are made with a "high
level template" or not. You still need UX if you everything with the basic
Cocoa controls or Windows standard widgets or whatever.

The kind of UIs you can do in native, beyond forms and wizards, the web can't
even dream of. The inverse is not true (except if you do your whole thing in
Canvas or WebGL which defeats the purpose).

------
z3t4
I find callbacks and events much easier to understand then promises and
futures. Example code:

    
    
      var req = get("https://news.ycombinator.com")
      req.onData = callbackFunction
      if(condition) req.abort()
    

"Callback hell" can be avoided by using named functions and sub-functions.

~~~
Mitranim
For one operation, sure. It doesn't work when you want to compose multiple
async operations, wait until all are finished, or race several of them to
completion (e.g. useful operation competing against timeout). The purpose of
promises/futures is this composability that allows you to get the order and
timing of operations right.

~~~
z3t4
What if you want to abort, or take another path, depending on an error code ?
With callbacks there's a convention that the first parameter is either null or
an error.

~~~
Mitranim
That's what the mapping operators are for: `.mapResult` (same as
`promise.then`), `.mapError` (same as `promise.catch`), or `.map`. The latter
has an errback signature, like Node.js callbacks. They can also return new
futures, transforming the result asynchronously. On top of that, error
handling in promises/futures is much easier than in callbacks, as you can use
one error handler for a chain of operations. Kinda like exceptions in
synchronous code.

------
ricardobeat
This looks great! Cancellable promises are still pending after 6+ years of
discussion.

I'm curious about the naming choices though, since user friendliness seems to
be one of the goals: why 'deinit' over 'cancel' and 'arrive' over 'resolve'?

~~~
Mitranim
We need a standard destructor interface instead of choosing a different word
every time. Cancel, close, destroy, drop, unmount, they do the same thing. If
we settled on ONE destructor interface, we could have automatic resource
management. [1] I use `deinit` in all my libraries, as it seems to be the most
neutral word appropriate for every case.

`arrive` — no particular reason. It's one "errback" method rather than two
methods like `resolve/reject`, so it needs to have a neutral tone. Not too
happy with it, better suggestions are welcome.

[1] Basic implementation of automatic resource management in JS:
[https://mitranim.com/espo/#-agent-value-](https://mitranim.com/espo/#-agent-
value-)

------
iheart2code
This is really great. It reminds me of NSOperationQueue on iOS. I think a cool
next step would be to add dependencies -- ie have Futures wait to execute
until dependent Futures have completed/succeeded.

~~~
Mitranim
Pretty much what jrs95 said. If I understand the question correctly,
`Future.all` and `Future.race` address exactly this case.

[1]
[https://github.com/Mitranim/posterus#futureallvalues](https://github.com/Mitranim/posterus#futureallvalues)

[2]
[https://github.com/Mitranim/posterus#futureracevalues](https://github.com/Mitranim/posterus#futureracevalues)

------
pspeter3
What is the overhead cost of maintaining the scheduler?

~~~
Mitranim
Negative. The scheduler is an optimisation. The alternative is to rely on VM
scheduling using `process.nextTick` or `setTimeout` for every new async
operation, which involves mandatory allocations and possibly other overhead.
Using a custom scheduler is MUCH more efficient, which is why every decent
promise implementation does it.

~~~
pspeter3
Interesting, I would have assumed that relying on VM scheduling would be more
efficient.

As a corollary, are custom promises faster than native promises?

~~~
Mitranim
Yes. Some popular promise polyfills totally trash the performance of "native"
promises. I don't quite remember which ones. Needless to say, Posterus also
compares very well in this field.

~~~
pspeter3
Hmm, I'll have to look for benchmarks then. My general experience is that it
is hard to outsmart the VM performance wise in the long run

~~~
Mitranim
We're not competing with VMs here. From what I hear, like many other built-
ins, "native" promises in every VM are implemented in JavaScript. They're not
always well done (V8 promises were really bad for a while), and pay a
mandatory overhead for useless "privacy" features dictated by the spec.

------
SpacePotatoe
Is it possible to use it with async/await?

~~~
Mitranim
Futures automatically coerce to promises, so yes.

Even better, they come with generator-based coroutines that work with futures,
and are automatically cancelable:
[https://github.com/Mitranim/posterus#routine](https://github.com/Mitranim/posterus#routine)

You can even run it with Koa 2 instead of async/await:
[https://github.com/Mitranim/koa-ring](https://github.com/Mitranim/koa-ring)

------
sova
Is canceling as useful as inhibitory neurons?

~~~
sova
I'm curious! If you are trying to cancel an async, why was it invoked in the
first place? Or is this for operations no longer requested, like a big upload
or something?

Inhibitory neurons make it possible for you to walk down stairs without
falling the whole way, so whatever trickster is voting down neurons, cut it
out

~~~
Mitranim
People and programs change their minds all the time. A lot of behaviors we
consider intuitive rely on some form of cancelation.

People gave a few examples in this subtopic:
[https://news.ycombinator.com/item?id=14962684](https://news.ycombinator.com/item?id=14962684)

~~~
sova
Perfect! Thank you.

------
slowmotarget
Well that was my quickest github star ever

------
JSONwebtoken
The brevity and aesthetics of async/await has made me reluctant to move back
to any pyramid type async chain. I don't know if it's good practice, but I use
typescript to declare an optional null type and just return a null from inside
the async function if I need to cancel.

~~~
Mitranim
Posterus provides coroutines just for that:
[https://github.com/Mitranim/posterus#routine](https://github.com/Mitranim/posterus#routine)

Posterus coroutines are similar to async/await, but cancelable and free of
`await`'s race condition problem (promise can get rejected before `await`
attaches a handler).

~~~
taralx
Why is that a race? IIRC Promise.then is supposed to invoke the callback
immediately (or on next loop) if the promise is already resolved.

~~~
Mitranim
It would be race-free if `.then()` was invoked synchronously when evaluating
the `await` expression, just like in normal Promise-based code. Currently in
V8, there's a delay between evaluating `await` and actually calling `.then()`.
If the promise uses a sufficiently nimble scheduler (i.e. based on
`process.nextTick`), its unhandled rejection handler may run _in between_,
throwing an exception, polluting stderr and possibly killing the process.

Example with Posterus:

    
    
      async function main() {
        try {
          await Future.fromError('fail')
        }
        catch (err) {
          console.error('caught:', err)
        }
      }
    

In Node, this actually produces an unhandled rejection because Posterus's
scheduler uses `process.nextTick` and squeezes into this unnecessary delay.
Doesn't happen if you `.catch()` manually or just use Posterus coroutines
instead of interoping with async/await, but it highlights the incorrect
implementation of async/await in the first place. (Or is the spec at fault?)

------
edem
Will there be a cancelable javascript sometime? I'd like to cancel it all.

