composer.js

'use strict';
/**
 * @fileOverview An efficient middleware composer for Koa.
 * @module composer
 */

const reuse = require('reusify');

/**
 * Filterable Middleware
 *
 * @example
 * {
 *     fn: (ctx, next) => ctx.user ? next() : ctx.throw(401),
 *     filtered: true,
 *     rich: false,
 *     test: "/user"
 * }
 *
 * @typedef {Object} FilterableMiddleware
 * @property {Function} fn Koa Middleware
 * @property {Boolean} [filtered=false] Whether or not this middleware should
 * actually be filtered.
 * @property {Boolean} [rich=false] Whether the filter for this middleware is
 * a function or a basic string for comparison.
 * @property {String|RegExp|Object} [test] Required when `filtered` is true.
 * Either a string for a `path.startsWith(...)` comparison or an object with
 * a `test` method that accepts the current path. Conveniently, compiled
 * regular expressions have just such a method.
 */

/**
 * Compose a method that will efficiently execute multiple middleware for
 * Koa. This makes use of the [reusify](https://www.npmjs.com/package/reusify)
 * module to avoid allocating excess objects and functions during runtime when
 * at all possible. As a result, memory churn should be reduced while V8 should
 * be able to properly optimize these methods.
 *
 * This can be used as a drop-in replacement for [koa-compose](https://github.com/koajs/compose).
 *
 * {@link FilterableMiddleware} objects can be supplied, describing middleware
 * that should be filtered to only run on specific paths.
 *
 * @example
 * app.use(compose(cors(), cache(), router.middleware()));
 *
 * @function
 * @name compose
 * @param {...(Function|Function[]|composer~FilterableMiddleware|FilterableMiddleware[])} input The various middleware
 * to compose together. This can consist of functions or arrays of functions. The
 * input may also be comprised of objects that describe middleware that should
 * be filtered to only run on specific paths.
 * @returns {Function} The composed middleware
 */
module.exports = function compose(...input) {
	const middlewares = [];

	for(const section of input) {
		if ( ! section )
			continue;

		for(const fn of Array.isArray(section) ? section : [section]) {
			if ( ! fn )
				continue;

			if ( typeof fn === 'function' )
				middlewares.push({
					filtered: false,
					fn
				});
			else if ( fn.fn )
				middlewares.push(fn);
			else
				throw new TypeError('invalid input to compose')
		}
	}

	const len = middlewares.length;

	function Composer() {
		this.i = null;
		this.context = null;
		this.next = null;

		const that = this,
			bound = [];

		this.run = function(i) {
			if ( i <= that.i )
				return Promise.reject(new Error('next() called multiple times'));

			const path = that.context.request.path;

			let fn;
			while(i <= len) {
				if ( i === len ) {
					fn = that.next;
					break;
				}

				const middleware = middlewares[i];
				if ( ! middleware.filtered ||
						(middleware.rich ?
							middleware.test.test(path) :
							path.startsWith(middleware.test)) ) {
					fn = middleware.fn;
					break;
				}

				i++;
			}

			this.i = i;

			if ( ! fn )
				return Promise.resolve();

			try {
				return Promise.resolve(fn(that.context, bound[i+1]));
			} catch(err) {
				return Promise.reject(err);
			}
		}

		// We want to bind our run function once for every middleware,
		// and once more after that to chain to the next() that the
		// compose middleware was itself called with.
		for(let i=0; i <= len; i++)
			bound.push(this.run.bind(this, i));
	}

	const pool = reuse(Composer);

	const ret = async function composed(context, next) {
		const inst = pool.get();

		inst.i = -1;
		inst.context = context;
		inst.next = next;

		try {
			return await inst.run(0)
		} finally {
			inst.context = null;
			inst.next = null;

			pool.release(inst);
		}
	}

	ret._middleware = middlewares;

	return ret;
}