神刀安全网

Async/await middleware composition

In this article, I am going to look at the possibility of composing asynchronous middleware in a similar way to Koa . All of the code can be transpiled by Babel using the presets for es2015 and stage-2 .

By now I am sure most people are up-to-date with the async specification for ES7. It allows for async defined functions to pause execution and await asynchronous calls to resolve before continuing. Very similar to ES6’s generators in their ability to pause execution, however async/await functionality is built on the back of the ES6 Promise specification.

Koa , the spiritual successor to Express , has always used generators to create an interesting approach to middleware enacting on a given context. Koa@2 looks to move more towards async/await with version 3 looking to remove support for generators completely.

Composition

Having played around with functional programming more diligently for the last year or so, especially concerning functional composition, I was looking for some way of combining async/await with composition to be able to create middleware similar to Koa.

The following is the code that I came up with, it is in no way perfect, but it has proved workable in most instances. It allows for the combination of both async and synchronous middlewares, however it is best to make sure synchronous middleware appears as the last item.

/**   * This symbol definition is used to determine whether the last argument  * passed into a middleware is already a defined `next` call.  *  * This allows for `compose(mw1, compose(mw2, mw3))` to work as intended.  */ const isNext = Symbol('isNext');  /**  * `middlewareWithContext` is variadic and takes middlewares as input.  * It returns a variadic function which is invoked with context(s) and then executes  * the middleware, currently `left-to-right`, returning a `Promise`.  */ const middlewareWithContext = (...mw) => (...args) =>   new Promise(async (resolve, reject) => {     /**       * The last `next` in the chain, should either call the `next` handler      * passed via `args` (denoting a continuation into another composition),      * or `resolve` the returned `Promise`.      */     const nxt = args[args.length - 1][isNext] ? args.pop() : () => resolve();     /**      * `await` execution of all the middleware provided, by reducing each      * supplied middleware and wrapping each function execution.      */     await mw.reduceRight((next, curr) =>       async function() {         /**          * Decorate each `next` handler with our `isNext` symbol to facilitate          * composition of compositions.          */         next[isNext] = true;         /**          * Wrap each call in `try/catch` and `reject` if an error is caught.          */         try {             await curr(...args.concat(next))         } catch(e) {           reject(e);           throw e;         }       }, nxt)();     /**      * Resolve the `Promise` if the middleware is `await`-ed and nothing is returned.      * We will get to this point if the middleware does not call the last `next` handler      * in the composition.      */     resolve();   });

As you can see there is not much to it, but it does allow us to write some async middleware. For example, with a simple timeout helper, we can see how this works and begin to experiment. Click here to see an example of some simple middleware using the timeout helper running on the Babel REPL .

Imitation is the sincerest form of flattery

Now lets look how we can expand on this to work with Node and its http module to produce something that works in a similar way to Koa , obviously without all the nicities involved with its plugins. We are going to be working with the raw Request and Response objects provided by Node’s http module.

As we want a similar approach to Koa, we are going to save responses on the body property of the context, so the first thing we need to provide is a middleware that will resolve the body property and send it back to the requesting client. The other beauty of Koa’s approach is the ability to capture errors high up the middleware stack, so lets also create a simple error handler middleware.

const resolve = async (ctx, next) => {   await next();   /* Simply end the response with the value in `ctx.body` or an empty string. */   ctx.res.end(ctx.body || ''); };  const error = async (ctx, next) => {   try { await next() }   catch(e) {     /* An error has occured so lets provide the message to the body. */     ctx.res.statusCode = 404;     ctx.body = e.message;   } };

OK, so now lets add a couple more middlewares for some simple routing. These are very basic as to show the approach that I took initially. For a more involved approach you can look at this gist which also attempts to mimic the Koa/Express API.

/* Define a middleware to handle `/foo`. */ const onFoo = async (ctx, next) => {   if (ctx.req.url === '/foo') {     ctx.res.statusCode = 200;     ctx.body = 'bar';   } else {     await next();   } };  /* Define a middleware to handle `/bar`. */ const onBar = async (ctx, next) => {   if (ctx.req.url === '/bar') {     ctx.res.statusCode = 200;     ctx.body = 'baz';   } else {     await next();   } };  /* Lets define a catch all that throws an error if hit. */ const notFound = async () => {   throw new Error('Route not found!'); };

Now using Node’s http module and the middleware composer we wrote earlier, it’s as simple as wiring the composed middleware into an http request handler.

import http from 'http';  http.createServer((req, res) =>   middlewareWithContext(resolve, error, onFoo, onBar, notFound)({ req, res }) ).listen(3000);

And that is it. You should be able to see the results we expect by browsing to /foo or /bar on localhost:3000 . Any other route will return the error message from our notFound function.

There is a lot more that can be done with this. Have a look at this gist to see an approach which matches the Koa/Express API more. The file core would be the library, in this case with the name nomad (you can ignore that), where as index would be the normal kind of entry point you write for an application. It’s not at all full-featured, but it was part of the experimentation to see how far I could take it. It also expands more on the "composition of compositions" that I touched on earlier.

Awaitable streams?

One other aspect of this I have been exprerimenting with is the possibility of using a similar approach for reactive streams. Instead of relying on a context, the next handler would be passed a value to execute the next "middleware" with (Yeah, it’s not really middleware at this point). This essentially allows for the containing of a value over time using a very declarative approach.

If this is at all interesting to you, let me know. I would love to hear from you.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Async/await middleware composition

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址