10 ms·
Show HN: Cancelable async primitives for JavaScript
- wging 9y agoVery 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 9y agoThanks 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 9y agoCan you compare/contrast with Fluture? https://github.com/fluture-js/Fluture https://github.com/fluture-js/Fluture
- Mitranim 9y agoThanks 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.
- iheart2code 9y agoThis 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.
- jrs95 9y agoI don't think I get what you mean exactly. How is this different from wrapping all the dependencies in a single promise and starting the dependent one when they're all finished?
- Mitranim 9y agoPretty 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
- sampl 9y agoMight 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 9y agoHere'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
- sampl 9y agoD'oh, thanks
- code-e 9y agoBut 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 9y agoInteresting! 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.
- deleted 9y ago[deleted]
- deleted 9y ago
- pspeter3 9y agoWhat is the overhead cost of maintaining the scheduler?
- Mitranim 9y agoNegative. 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 9y agoInteresting, I would have assumed that relying on VM scheduling would be more efficient. As a corollary, are custom promises faster than native promises?
- Mitranim 9y agoYes. 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 9y agoHmm, 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 9y agoWe'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.
- JSONwebtoken 9y agoThe 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 9y agoPosterus 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 9y agoWhy 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 9y agoIt 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?)
- slowmotarget 9y agoWell that was my quickest github star ever
- littlecranky67 9y agoAm 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 9y agoGood 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 9y agoWhat 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 9y agoSort of answered above: https://news.ycombinator.com/item?id=14963152 https://news.ycombinator.com/item?id=14963152
- Mitranim 9y agoI 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.
- shados 9y agoOkay. Most.js then, at 10kb minified looking at the build on unpkg. Also, implementing an observable from scratch is pretty much trivial. The operators are where all of the logic are, and they are arguably simpler to understand than alternatives because of how well documented they are. And if you limit yourself to the operators that are equivalent to what this library provides, there really wouldn't be much complexity to it.
- jzig 9y agoI 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?
- deleted 9y ago[deleted]
- Mitranim 9y agoHere'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/1c54481d4760a7eb730eb3dbe2e861abb26b68a9/lib/epdf.js#L37 https://github.com/Mitranim/epdf/blob/1c54481d4760a7eb730eb3...
- antjanus 9y agoThis 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.
- SpacePotatoe 9y agoIs it possible to use it with async/await?
- Mitranim 9y agoFutures 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
- inglor 9y agoYou can also use cancellation tokens with promises. Pretty easy to do and doesn't require a library.
- Rapzid 9y agoThere 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 9y agoYes, 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.
- Mitranim 9y agoNot familiar with cancelation tokens. Can you describe the approach?
- mikewhy 9y agoYou pass them to an asynchronous function and said function can do something like `token.throwIfCancelled()` if you've called `token.cancel()` elsewhere.
- Mitranim 9y agoDoesn't sound as ergonomic as just returning a cancelation function in a future initialiser. Not seeing any other advantages, either. Am I missing some?
- calafrax 9y agothis 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 9y agoSeconded. 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 9y agoDepends 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
- ricardobeat 9y agoThis 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 9y agoWe 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-
- mpodlasin 9y agoJust 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 9y agoThis 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 9y agoimport { 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
- tannhaeuser 9y agoI'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_overwhelmed_with_the_current_web/ https://www.reddit.com/r/webdev/comments/6sdglh/feeling_over...
- ivan_gammel 9y agoUser 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 9y ago>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 9y agoUser 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 9y ago>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).
- sova 9y agoIs canceling as useful as inhibitory neurons?
- sova 9y agoI'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 9y agoPeople 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 9y agoPerfect! Thank you.
- edem 9y agoWill there be a cancelable javascript sometime? I'd like to cancel it all.
- z3t4 9y agoI 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 9y agoFor 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 9y agoWhat 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 9y agoThat'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.
- a-saleh 9y agoIsn't callback hell referencing to the fact that you can't easily access the previous result of computation without nesting closures inside one another? It seems to me that first time I really avoided cb hell is when I started using async/await.
- 9y ago