diff --git a/lib/application.js b/lib/application.js index e931a30..cd3d3fb 100644 --- a/lib/application.js +++ b/lib/application.js @@ -8,7 +8,7 @@ const debug = require('debug-ms')('koa:application'); const onFinished = require('./onfinish'); const response = require('./response'); -const compose = require('koa-compose'); +const compose = require('./compose'); const isJSON = require('./isjson'); const context = require('./context'); const request = require('./request'); diff --git a/lib/compose.js b/lib/compose.js new file mode 100644 index 0000000..87433d0 --- /dev/null +++ b/lib/compose.js @@ -0,0 +1,52 @@ +/** + * Lifted from koa-compose package. + */ + +'use strict'; + +/** + * Compose `middleware` returning + * a fully valid middleware comprised + * of all those which are passed. + * + * @param {Array} middleware + * @return {Function} + * @api public + */ + +function compose(middleware) { + if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!'); + for (const fn of middleware) { + if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!'); + } + + /** + * @param {Object} context + * @return {Promise} + * @api public + */ + + return function(context, next) { + // last called middleware # + let index = -1; + return dispatch(0); + function dispatch(i) { + if (i <= index) return Promise.reject(new Error('next() called multiple times')); + index = i; + let fn = middleware[i]; + if (i === middleware.length) fn = next; + if (!fn) return Promise.resolve(); + try { + return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); + } catch (err) { + return Promise.reject(err); + } + } + }; +} + +/** + * Expose compositor. + */ + +module.exports = compose; diff --git a/package.json b/package.json index bfe7ea0..dd502ce 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "debug-ms": "~4.1.2", "fresh": "~0.5.2", "http-errors-lite": "^2.0.2", - "koa-compose": "^4.1.0", "type-is": "^1.6.16" }, "devDependencies": { diff --git a/test/utils/compose.js b/test/utils/compose.js new file mode 100644 index 0000000..a82c32b --- /dev/null +++ b/test/utils/compose.js @@ -0,0 +1,354 @@ +'use strict'; + +/* eslint-env jest */ + +const compose = require('../../lib/compose'); +const assert = require('assert'); + +function wait(ms){ + return new Promise(resolve => setTimeout(resolve, ms || 1)); +} + +function isPromise(x){ + return x && typeof x.then === 'function'; +} + +describe('Koa Compose', () => { + it('should work', async() => { + const arr = []; + const stack = []; + + stack.push(async(context, next) => { + arr.push(1); + await wait(1); + await next(); + await wait(1); + arr.push(6); + }); + + stack.push(async(context, next) => { + arr.push(2); + await wait(1); + await next(); + await wait(1); + arr.push(5); + }); + + stack.push(async(context, next) => { + arr.push(3); + await wait(1); + await next(); + await wait(1); + arr.push(4); + }); + + await compose(stack)({}); + assert.deepEqual(arr, [1, 2, 3, 4, 5, 6]); + }); + + it('should be able to be called twice', () => { + let stack = []; + + stack.push(async(context, next) => { + context.arr.push(1); + await wait(1); + await next(); + await wait(1); + context.arr.push(6); + }); + + stack.push(async(context, next) => { + context.arr.push(2); + await wait(1); + await next(); + await wait(1); + context.arr.push(5); + }); + + stack.push(async(context, next) => { + context.arr.push(3); + await wait(1); + await next(); + await wait(1); + context.arr.push(4); + }); + + const fn = compose(stack); + const ctx1 = { arr: [] }; + const ctx2 = { arr: [] }; + const out = [1, 2, 3, 4, 5, 6]; + + return fn(ctx1).then(() => { + assert.deepEqual(out, ctx1.arr); + return fn(ctx2); + }).then(() => { + assert.deepEqual(out, ctx2.arr); + }); + }); + + it('should only accept an array', () => { + let err; + try { + compose(); + throw new Error('should not be called'); + } catch (e) { + err = e; + } + return assert(err instanceof TypeError); + }); + + it('should create next functions that return a Promise', () => { + const stack = []; + const arr = []; + for (let i = 0; i < 5; i++) { + stack.push((context, next) => { + arr.push(next()); + }); + } + + compose(stack)({}); + + for (let next of arr) { + assert(isPromise(next), 'one of the functions next is not a Promise'); + } + }); + + it('should work with 0 middleware', () => { + return compose([])({}); + }); + + it('should only accept middleware as functions', () => { + let err; + try { + compose([{}]); + throw new Error('should not be called'); + } catch (e) { + err = e; + } + return assert(err instanceof TypeError); + }); + + it('should work when yielding at the end of the stack', async() => { + let stack = []; + let called = false; + + stack.push(async(ctx, next) => { + await next(); + called = true; + }); + + await compose(stack)({}); + assert(called); + }); + + it('should reject on errors in middleware', () => { + let stack = []; + + stack.push(() => { throw new Error(); }); + + return compose(stack)({}) + .then(() => { + throw new Error('promise was not rejected'); + }) + .catch(e => { + assert(e instanceof Error); + }); + }); + + it('should work when yielding at the end of the stack with yield*', () => { + let stack = []; + + stack.push(async(ctx, next) => { + await next; + }); + + return compose(stack)({}); + }); + + it('should keep the context', () => { + const ctx = {}; + + const stack = []; + + stack.push(async(ctx2, next) => { + await next(); + assert.strictEqual(ctx2, ctx); + }); + + stack.push(async(ctx2, next) => { + await next(); + assert.strictEqual(ctx2, ctx); + }); + + stack.push(async(ctx2, next) => { + await next(); + assert.strictEqual(ctx2, ctx); + }); + + return compose(stack)(ctx); + }); + + it('should catch downstream errors', async() => { + const arr = []; + const stack = []; + + stack.push(async(ctx, next) => { + arr.push(1); + try { + arr.push(6); + await next(); + arr.push(7); + } catch (err) { + arr.push(2); + } + arr.push(3); + }); + + stack.push(async(ctx, next) => { + arr.push(4); + throw new Error(); + }); + + await compose(stack)({}); + assert.deepEqual(arr, [1, 6, 4, 2, 3]); + }); + + it('should compose w/ next', () => { + let called = false; + + return compose([])({}, async() => { + called = true; + }).then(() => { + assert(called); + }); + }); + + it('should handle errors in wrapped non-async functions', () => { + const stack = []; + + stack.push(() => { + throw new Error(); + }); + + return compose(stack)({}).then(() => { + throw new Error('promise was not rejected'); + }).catch(e => { + assert(e instanceof Error); + }); + }); + + // https://github.com/koajs/compose/pull/27#issuecomment-143109739 + it('should compose w/ other compositions', () => { + let called = []; + + return compose([ + compose([ + (ctx, next) => { + called.push(1); + return next(); + }, + (ctx, next) => { + called.push(2); + return next(); + } + ]), + (ctx, next) => { + called.push(3); + return next(); + } + ])({}).then(() => assert.deepEqual(called, [1, 2, 3])); + }); + + it('should throw if next() is called multiple times', () => { + return compose([ + async(ctx, next) => { + await next(); + await next(); + } + ])({}).then(() => { + throw new Error('boom'); + }, err => { + assert(/multiple times/.test(err.message)); + }); + }); + + it('should return a valid middleware', () => { + let val = 0; + return compose([ + compose([ + (ctx, next) => { + val++; + return next(); + }, + (ctx, next) => { + val++; + return next(); + } + ]), + (ctx, next) => { + val++; + return next(); + } + ])({}).then(() => { + assert.strictEqual(val, 3); + }); + }); + + it('should return last return value', () => { + const stack = []; + + stack.push(async(context, next) => { + let val = await next(); + assert.strictEqual(val, 2); + return 1; + }); + + stack.push(async(context, next) => { + const val = await next(); + assert.strictEqual(val, 0); + return 2; + }); + + const next = () => 0; + return compose(stack)({}, next).then(val => { + assert.strictEqual(val, 1); + }); + }); + + it('should not affect the original middleware array', () => { + const middleware = []; + const fn1 = (ctx, next) => { + return next(); + }; + middleware.push(fn1); + + for (const fn of middleware) { + assert.equal(fn, fn1); + } + + compose(middleware); + + for (const fn of middleware) { + assert.equal(fn, fn1); + } + }); + + it('should not get stuck on the passed in next', () => { + const middleware = [(ctx, next) => { + ctx.middleware++; + return next(); + }]; + const ctx = { + middleware: 0, + next: 0 + }; + + return compose(middleware)(ctx, (ctx, next) => { + ctx.next++; + return next(); + }).then(() => { + assert.strictEqual(ctx.middleware, 1); + assert.strictEqual(ctx.next, 1); + }); + }); +});