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

es7 async function feedback #415

Closed
jonathanong opened this issue Feb 16, 2015 · 97 comments
Closed

es7 async function feedback #415

jonathanong opened this issue Feb 16, 2015 · 97 comments
Assignees
Milestone

Comments

@jonathanong
Copy link
Member

es7 async functions are now supported when you enabled app.experimental = true. it uses https://github.com/thenables/composition. however, the last time i benchmarked it, it was significantly slower.

please provide feedback and maybe do some benchmarks and performance improvements so we can one day support es7 async functions by default without sacrificing anything :)

@juliangruber
Copy link
Contributor

👍

@mpal9000
Copy link

🍻

@calebboyd
Copy link

This looks awesome. I did some dabbling with a different composing mechanism with only promises and async await. Thought maybe I should post it here for feedback

function compose (middleware) {
  let responses;
  let next;
  middleware = middleware.map((mw, i) => {
    return function (){
      next = () => (middleware[i+1] || noop).call(this)
      return responses[i] = mw.call(this, next)
    };
  });
  return function composed () {
    responses = [];
    return middleware[0].apply(this, arguments)
      .then(() => Promise.all(responses));
  };
}

gist

Using babel with bluebirdCoroutines enabled, this method achieved better marks than the default implementation (koa-compose). Its also a little (lot?) different in that you must invoke next but I think that gives more control (concurrent vs sequential etc.)

Thoughts/Criticisms?

@tj
Copy link
Member

tj commented Mar 5, 2015

I wouldn't want to change the next-ing API-wise, it's useful to allow middleware to break the down stream to short-circuit.

Perf-wise I'd have to see some numbers! That's not really a huge concern though, less deps/complexity is a nicer win. We're already achieving numbers that would sustain most applications with a single node, and no production app will have a single node so that's less of a concern.

@calebboyd
Copy link

@tj Hah, good point...
On a really slow VM..
Standard Azure D2 (2 cores 7Gb ram) with Ubuntu 14.04 - node 0.12.0

#MW koa koa async await bluebirdCoroutines koa async await asyncToGenerator
1 3392.4 3422.7 2180.7
5 3109.8 3303.7 1751.6
10 3128 3223 1460.7
20 2889 2988 1039
50 2630 2635.6 593.6

This could be quite different in versions of io.js (for native promises)
Since bluebird certainly isn't lightweight this doesn't do much for shedding dependencies...

I'm thinking I can still short-circuit with this mechanism too..
are you suggesting upstream be invoked all together?

I should mention that this pattern is not my own. Its copied from the way OWIN and Katana (.NET) handle their async await middleware.

@zensh
Copy link
Contributor

zensh commented Mar 5, 2015

I don't like next-ing API, it will lead to another callback hell.
However, next-ing API is core mechanism in koa that can't be disable, so I create a toa that similar to koa.

@tj sorry~

@tj
Copy link
Member

tj commented Mar 5, 2015

Next probably won't be going anywhere, having proper middleware is a lot more flexible than the Express style, but use whatever works for you obviously!

@Globegitter
Copy link

@jonathanong Just wanting to get started doing some performance testing and having issues getting the new syntax to work:

var koa = require('koa');
var app = koa();

app.experimental = true

// logger
app.use(async function (next){
  var start = new Date;
  await next;
  var ms = new Date - start;
  console.log('%s %s - %s ms', this.method, this.url, ms);
});

// response
app.use(async function (){
  this.body = 'Hello World';
});

app.listen(3000);

Is this supposed to work? Or did I misunderstand something with the syntax?

@hallas
Copy link
Contributor

hallas commented Mar 5, 2015

What error are you getting?

@Globegitter
Copy link

Running on io.js 1.2:

/Users/markus/Projects/Artificial/koa-test/index.js:19
app.use(async function (next){
              ^^^^^^^^
SyntaxError: Unexpected token function
    at exports.runInThisContext (vm.js:53:16)
    at Module._compile (module.js:429:25)
    at Object.Module._extensions..js (module.js:464:10)
    at Module.load (module.js:341:32)
    at Function.Module._load (module.js:296:12)
    at Function.Module.runMain (module.js:487:10)
    at startup (node.js:111:16)
    at node.js:799:3

@jonathanong
Copy link
Member Author

sorry, instructions are probably unclear. you need to use babel to enable async functions. here's the test file as an example:

https://github.com/koajs/koa/blob/master/test/experimental/index.js

@Globegitter
Copy link

Ahhhh, I was almost thinking that. But also a bit surprised because babel was made a devDependency with the latest version. So we should install that manually for the time being in our project?

@dougwilson
Copy link
Contributor

So we should install that manually for the time being in our project?

Yes

@Globegitter
Copy link

Ok thanks all working now - let the happy testing begin :)

@ide
Copy link
Contributor

ide commented May 4, 2015

@calebboyd would you happen to have your benchmark code laying around? I'm curious as to why asyncToGenerator is so much slower than plain koa since the two should be doing roughly the same thing. As I understand it, both wrap native generator functions to create functions that return promises, using the built-in Promise class.

@jonathanong
Copy link
Member Author

@calebboyd
Copy link

@ide the benchmark I had above used promise based middleware instead of the included genereator based. The comparable performance I believe is due to koa using plain generators (not wrapped, good performance). Where asyncToGenerator wraps generators in promises, which is then wrapped again by the composition function. But when using promise based mw and bluebird, intermediate promise and closure allocations are avoided (bluebird optimization).

@ide
Copy link
Contributor

ide commented May 5, 2015

Thanks, guys.

@mgenev
Copy link

mgenev commented May 31, 2015

@calebboyd any chance that example can go public on github?

@calebboyd
Copy link

@mgenev What are you looking for exactly? The basic gist is posted above. The problem is that it breaks koa's generator based api and consequently anything implemented with it :/ . mho as expressed in #420 Is that promise based middleware should be used if koa uses async await. As it seems we might eventually get async await optimization -- but that probably won't apply to 'thenables'

I can probably add a fork with the changes...

@mgenev
Copy link

mgenev commented May 31, 2015

@calebboyd I got a lot of it working with bluebird coroutines and the composition from the OP, but when i start routing I'm getting confusing errors like
Unhandled rejection Error: Can't set headers after they are sent. or undefined is not a function with 'asyncToGenerator' , I think it's because koa-router is dependent on koa-compose, not the above mentioned composition module.

I also tried with koa-route, but it also breaks with async functions, I'm guessing both of those modules have to be rewritten for async/await?

@thelinuxlich
Copy link

Better use something like Earl Grey: http://breuleux.github.io/earl-grey/ while the native option is unavailable

@ralyodio
Copy link

ralyodio commented Jun 1, 2015

You can use babel on the server side to get async/await. (i have not done this though, so I can't be of much help).

@cesarandreu
Copy link

Was playing around with async/await in koa, I keep getting A promise was converted into a generator, which is an anti-pattern. Please avoid usingyield* next!, and I'm not sure why. I'm gonna look into it later.

None of the routers work well with async functions :(. I'm using koa-router, and as a stopgap I'm doing:

const routes = new Router().get('/foo', compose([a, b, c]))

@mgenev
Copy link

mgenev commented Jun 15, 2015

@cesarandreu That was my experience as well, so I opened an issue which is referenced above, but so far no response. It's kind of a silly situation because I've ben able to use async/await in an express app, but not koa which is built for the future JS

@dmtrs
Copy link

dmtrs commented Jun 15, 2015

@cesarandreu Could you share more on what you are actually doing in [ a, b, c ] functions?

@tj
Copy link
Member

tj commented Sep 17, 2015

nice :D

@ruimarinho
Copy link
Contributor

This looks really good! Nice work.

app.use(async next => {
  await next()
});

@jonathanong
Copy link
Member Author

@ruimarinho i don't believe that'll work because this isn't passed into the => function.

@ruimarinho
Copy link
Contributor

Hmm, it did, because I've just tested it with koa-next-as-function/async-await.js.

@tj
Copy link
Member

tj commented Sep 17, 2015

Ah boo I was thinking it was just lexically bound by default, couldn't remember, guess not. Maybe passing ctx is the way to go then. Need a skinny arrow :( moar ES features

@ide
Copy link
Contributor

ide commented Sep 17, 2015

One thing I like about passing ctx around is that it frees up this so that your middleware can be an object's method like this:

class KoaResponseCompressor {
  constructor(options) { ... }
  async compressResponseAsync(ctx, next) {
    await next(ctx);
    ...
    this.helperMethod();
    ...
  }
  helperMethod() { ... }
}

export default function middleware(options) {
  return ::new KoaResponseCompressor(options).compressResponseAsync;
}
import responseCompressor from 'koa-response-compressor';
app.use(responseCompressor(options));

@tj
Copy link
Member

tj commented Sep 17, 2015

@ide good point. It's definitely the more elegant solution, arg or not. Only other alternative I can think of at the moment (which I don't like haha) is hanging it off ctx.next

@calebboyd
Copy link

Really neat @ide

Just a note on some specs...
Currently async/await (to generator) won't work with anything but transpiled arrow functions. There is no such thing as a GeneratorArrowFunction. But there is a proposal for the AsyncArrowFunction.

Because of that, you can't use native => from Node 4 unless you opt in to using slower regenerator transformations. See issue here: babel/babel#2342

@tj, The composing function I set up implements both of those options
https://github.com/calebboyd/app-builder

@mpal9000
Copy link

+1 for passing ctx as an argument:

  • more functional, more flexible
  • problems in this discussion
  • closer to page.js, ie. easier to create a more portable/isomorphic solution
  • closer to express, ie. easier adoption

For me koa with a more functional style (this change and maybe without prototypes) and async functions will be close to perfection :P

@gyson
Copy link
Member

gyson commented Sep 19, 2015

Just realized that using ctx as argument would work very nice with destructuring.

app.use(async ({request, response}, next) => {
  // ...
  await next()
  // ...
})

Not sure if this is really useful, but worth sharing.

@jonathanong
Copy link
Member Author

koajs/compose#27 if anyone wants to check it out

@jonathanong jonathanong added this to the 2.0.0 milestone Sep 20, 2015
@smcmurray
Copy link

What's the strategy to create compatibility with existing middleware that doesn't expect a context as an argument?

@hiroqn
Copy link

hiroqn commented Oct 7, 2015

For the compatibility,
when passing ctx as arguments, second arguments is better.
And at the same time, bind this for ctx.

And when using non-GeneratorFunction, next is instance of Promise is easy to understand, i feel.

app.use(function *(next) {
  this.body += ':b';
  yield next;
})

app.use(async (next, ctx) => {
  ctx.body += ':D';
  await next;
});

app.use(async function(next) => {
  this.body += ':)';
  await next;
});

app.use((next, ctx) => {
  ctx.body += ':D';
  return next;
});

app.use(function(next) => {
  this.body += ':)';
  return next;
});

@smcmurray
Copy link

@hiroqn , the argument order is set. Context is passed first because it is required and next is optional. Not binding to this allows comments like @ide's above.

As for making next a promise, next is a middleware function. It has to be invoked. Your examples never invoke next. Indeed, they seem to rely on the next middleware having already been invoked. This doesn't makes sense.

return next(ctx);

This will still return a promise.

@felixfbecker
Copy link
Contributor

I really think the concept of calling the next middleware function explicitly and awaiting it's promise is much easier to understand for newcomers than yielding next without calling.
Also, I think it should simplify unit testing of middleware.
So, what is the ETA for this? Can I help out somehow?

@smcmurray
Copy link

@jonathanong, can koajs/compose#27 wrap up into some pre-release version right away and then provide a koa pre-release branch that uses the koa-compose pre-release?

Having this available ASAP would help it mature into something releasable.

@ruimarinho
Copy link
Contributor

@smcmurray @jonathanong what about releasing it as 3.0.0-alpha on npm?

@mike-marcacci
Copy link

I would be very happy about a 3.0.0-alpha release of this right now: I'm setting up the foundation for a project that won't see production for a little bit but needs to build developer consensus sooner rather than later.

I'm a pretty big fan of this proposal, so I'd love to start out in this direction!

@thelinuxlich
Copy link

Once it ships I'm using it!

@jonathanong
Copy link
Member Author

please test this! #530

@Globik
Copy link

Globik commented Dec 24, 2015

Is still there a ctx.state?

@jonathanong
Copy link
Member Author

not related to es7 async functions. if there isn't, you could always do this.state = this.state || {};. can you open a new issue?

@koajs koajs locked and limited conversation to collaborators Dec 24, 2015
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests