compose icon indicating copy to clipboard operation
compose copied to clipboard

support variadic usage (rather than requiring an array)

Open stephenmathieson opened this issue 8 years ago • 9 comments

this is just my ocd, but imo the latter looks much nicer:

app.use(route.get('/foo', compose([ middleware1, middleware2, routeHandler ])))

vs

app.use(route.get('/foo', compose(middleware1, middleware2, routeHandler)))

would you take a pr implementing this in a backward compatible way?

/cc @jonathanong

stephenmathieson avatar Oct 21 '16 22:10 stephenmathieson

i would personally like:

function compose(...args) {
  const middleware = _.flatten(args)
  // ...
}

@tj @juliangruber @dead-horse @fengmk2 thoughts?

jonathanong avatar Oct 21 '16 22:10 jonathanong

SGTM

tj avatar Oct 21 '16 23:10 tj

Awesome- I'll throw something together Monday if not sooner.

stephenmathieson avatar Oct 22 '16 05:10 stephenmathieson

gonna hold on this for now... but i don't mind publishing this as v4 after we've understood the use-case for mutating middleware

jonathanong avatar Oct 26 '16 18:10 jonathanong

Any updates? Can we just check if the first item is an array?

const _middlewares = Array.isArray(middlewares[0])
    ? middlewares[0]                              
    : middlewares                                 

lewisdiamond avatar Mar 03 '18 02:03 lewisdiamond

IMO, it can be better if it supports both middleware and middlewares array compose([middleware1, middleware2], middleware3, [middleware4, middleware5])

BTW, it is overweight to use lodash

Runrioter avatar Mar 03 '18 17:03 Runrioter

@Runrioter I'm not sure if that's really useful, but you can always add a check that composes them

if (Array.isArray(my_middleware_taken_from_input_list)) { middleware = compose(middleware) }

lewisdiamond avatar Mar 03 '18 19:03 lewisdiamond

With this you get better performance (see below) and support both compose([m1,m2]) and compose(m1,m2). It passes all the tests but many of them need to be rewritten if you keep the unwrapped case for compose(m1), it may still hide bugs too.

function compose(...middlewares) {
    const _middlewares =
        Array.isArray(middlewares[0]) && middlewares.length === 1
        ? middlewares[0]
        : middlewares
    _middlewares.map(m => {
        if (typeof m !== "function")
            throw new TypeError("Middleware must be composed of functions!")
    })

    const noop = () => {}
    function _next(context, next, end) {
        let called = 0
        return Promise.resolve(
            next === _middlewares.length
            ? end(context, noop)
            : _middlewares[next](context, () => {
                if (!called++) return _next(context, next + 1, end)
                else throw new Error("next() called multiple times")
            })
        )
    }
    return function composed(context, next) {
        try {
            //Unwrap the case where there's a single middleware
            //It can catch test cases so tests need to be updated to test
            //with 2 or more middlewares as well as just 1.
            if (_middlewares.length === 1) {
                let called = 0
                return Promise.resolve(
                    _middlewares[0](context, () => {
                        if (!called++) return (next || noop)(context, noop)
                        else throw new Error("next() called multiple times")
                    })
                )
            // END
            } else return Promise.resolve(_next(context, 0, next || noop))
        } catch (err) {
            return Promise.reject(err)
        }
    }
}

Current implementation:

       1,130,866 op/s » (fn * 1)
         556,355 op/s » (fn * 2)
         280,713 op/s » (fn * 4)
         154,546 op/s » (fn * 8)
          76,328 op/s » (fn * 16)
          39,178 op/s » (fn * 32)
          18,403 op/s » (fn * 64)
           9,453 op/s » (fn * 128)
           4,572 op/s » (fn * 256)
           2,251 op/s » (fn * 512)
           1,042 op/s » (fn * 1024)

This implementation:

       1,308,892 op/s » (fn * 1) -- 15.7% (11.3% if you removed the optimization)
         610,887 op/s » (fn * 2)  -- 9.8%
         303,005 op/s » (fn * 4)  -- 7.9%
         158,465 op/s » (fn * 8)  -- 2.5% (Probably within margin of error from here on)
          77,234 op/s » (fn * 16) -- 1.2%
          38,029 op/s » (fn * 32) -- -3%
          18,585 op/s » (fn * 64) -- irrelevant
           9,410 op/s » (fn * 128)
           4,578 op/s » (fn * 256)
           2,232 op/s » (fn * 512)
           1,072 op/s » (fn * 1024)

lewisdiamond avatar Mar 04 '18 02:03 lewisdiamond

I forgot the PR that added this got reverted and wasted a bunch of time debugging... any chance we'd be up for trying this again?

Also, any news on "the use-case for mutating middleware"?

stephenmathieson avatar Sep 06 '20 02:09 stephenmathieson