Skip to content

Instantly share code, notes, and snippets.

@aronanda
Last active November 24, 2022 01:29
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aronanda/d31bb47918145a5aace6005f172e035d to your computer and use it in GitHub Desktop.
Save aronanda/d31bb47918145a5aace6005f172e035d to your computer and use it in GitHub Desktop.
A minimal Javascript Middleware Pattern Implementation with support for custom parameters, chainable use() method and a definable object. Based on https://gist.github.com/darrenscerri/5c3b3dcbe4d370435cfa
class Middleware {
constructor(obj) {
obj = obj || this
Object.defineProperty(this, '__obj', { value: obj })
Object.defineProperty(this, 'go', { value: function go(...args) {
args[args.length - 1].apply(obj, args.slice(0, -1))
}, writable: true })
}
use(fn) {
this.go = (stack => (...args) => stack(...args.slice(0, -1), () =>
fn.call(this.__obj, ...args.slice(0, -1), args[args.length - 1]
.bind(this.__obj, ...args.slice(0, -1)))))(this.go)
return this
}
}
class Middleware {
constructor(obj, method = 'go') {
let extras = []
const stack = []
const go = async (...args) => {
let done = args[args.length - 1]
if (typeof done === 'function') {
args.pop()
if (obj) done = done.bind(obj)
} else done = null
try {
for (let fn of stack) {
if (obj) fn = fn.bind(obj)
extras = await new Promise((resolve, reject) => {
fn(...args.concat(extras), function next(err, ...extras) {
if (err) return reject(err)
resolve(extras)
})
})
}
args = args.concat(extras)
if (done) done(...args)
return args
} catch (err) { throw err }
}
Object.defineProperty(this, method, { value: go })
Object.defineProperty(this, 'use', { value: fn => stack.push(fn) })
}
}
const mw = new Middleware()
mw.use((a, b, next) => {
log('calling 1st func', { a, b })
next()
})
mw.use((a, b, next) => {
log('calling 2nd func', { a, b })
next(null, 'c')
})
mw.use((a, b, c, next) => {
log('calling 3rd func', { a, b, c })
if (Math.random() > 0.5)
next(new Error('uh oh'), c)
else
next(null, c)
})
mw.use((a, b, c, next) => {
log('calling 4th func', { a, b, c })
next(null, c)
})
try {
let result = await mw.go('a', 'b', (a, b, c) => log('done', { a, b, c }))
log(result)
} catch (err) {
log('err', err.toString())
}
const middleware = new Middleware(/* define object or defaults to this */)
middleware.use(function (req, res, next) {
setTimeout(() => {
this.hook1 = true
req.step++
res.step++
next()
}, 10)
})
middleware.use(function (req, res, next) {
setTimeout(() => {
this.hook2 = true
req.step++
res.step++
next()
}, 10)
}).use((req, res, next) => {
// chainable
setTimeout(() => {
req.step++
res.step++
next()
}, 10)
})
const start = new Date()
const req = Object.create(new class Req {}(), { step: { value: 1, enumerable: true, writable: true }})
const res = Object.create(new class Res {}(), { step: { value: 1, enumerable: true, writable: true }})
middleware.go(req, res, function (req, res) {
console.log(this) // Middleware { hook1: true, hook2: true }
console.log(req) // Req { step: 4 }
console.log(res) // Res { step: 4 }
console.log(new Date() - start) // around 30ms
})
@mStirner
Copy link

mStirner commented Feb 18, 2021

Wonderful. Perfect, exactly what im looking for while i found https://gist.github.com/darrenscerri/5c3b3dcbe4d370435cfa
Well done!

EDIT: Is there a explanation for this use function / stack creation.
Im having trouble to understand how out of that the callback stack is created.
I want to add a "Abort" method, when a error is passed as first argument to the next function. While i try to understand that, it gives me a brain fu*k...

@aronanda
Copy link
Author

aronanda commented Feb 28, 2021

I hear you w/ the brain f'cking -- I don't know how long I spent on wrapping my head around this, but it was long.
Basically, it's just chaining each next function together, when you call use(fn) it's making that fn the next next() : )
The arguments that go into the fn are arbitrary while the last argument will always be the 'next' callback.
That part with the ...args.slice(0, -1) is basically taking your arbitrary arguments from the previous callback and putting them on the next callback (next()). You might be able to do a check there before passing the args to the "next" callback.
~ mind f*ky indeed )

perhaps something like this:

// inside use method
this.go = (stack => (...args) => stack(...args.slice(0, -1), () => {
  const nextArgs = args.slice(0, -1)
  const firstArg = nextArgs[0]
  if (firstArg && firstArg instanceof Error)
    throw firstArg
  return fn.call(this.__obj, ...nextArgs, args[args.length - 1].bind(this.__obj, ...nextArgs))))(this.go)
}
return this

let me know what you figure out, I was just about to need that feature actually

@mStirner
Copy link

I somehow hacked this piece of code together, without a fucking clue whats going on ;)
Added a "abortion" catch, when you pass a error argument to next call.
When a "catcher" function is set, the catcher function gets called with the passed error and not the next use callback.

And a other usefull feture, "overirde passed arguments".

use((A, B, C, next) => {
       next(null, A, {data: true, ...B});
});

use((A, B, C, next) => {
      // B is now a new object with the properties of original B
     // And a new one "data"
       console.log(A, B, C)
       next();
});

The whole middleware class

class Middleware {

  constructor(obj) {

    this.catcher = null;
    obj = obj || this;

    Object.defineProperty(this, "__obj", {
      value: obj
    });

    Object.defineProperty(this, "start", {
      value: (...args) => {

        let cb = args[args.length - 1];
        let params = args.slice(0, -1);

        cb.apply(obj, params);

      },
      writable: true
    });

  };


  use(fn) {
    this.start = (stack => {
      return (...args) => {


        stack(...args.slice(0, -1), (...override) => {

          args.splice(0, override.length, ...override);

          let next = (err, ...override) => {

            if (err instanceof Error && this.catcher) {
              return this.catcher(err);
            } else if (!(err == undefined || err == null)) {
              throw new Error(`First argument should be null/undefined or error instance, got: ${err}`);
            }

            args.splice(0, override.length, ...override);
            args[args.length - 1].bind(this.__obj, ...args.slice(0, -1))();

          };

          fn.call(this.__obj, ...args.slice(0, -1), next);

        });


      };
    })(this.start);
  };

  catch(fn) {
    this.catcher = fn;
  };

};

Not perfect, but serve my needs.
if you have any improvements, please let me know.

@aronanda
Copy link
Author

aronanda commented Mar 1, 2021

Nice, I see what you did there. Would be good to pass results to the following middlewares. Here's my latest solution that allows that and throws an error if one is present as the first argument of the next function. I used promises instead to make it easier to read (much easier). And I made the object binding optional as you oftentimes want to bind your own functions. Also, the result is returned at the end, regardless of whether you include a final callback in the go method (or rename the go method to something else ;)

class Middleware {
  constructor(obj, method = 'go') {
    let extras = []
    const stack = []
    const go = async (...args) => {
      let done = args[args.length - 1]
      if (typeof done === 'function') {
        args.pop()
        if (obj)
          done = done.bind(obj)
      } else
        done = null
      try {
        for (let func of stack) {
          if (obj)
            func = func.bind(obj)
          extras = await new Promise((resolve, reject) => {
            func(...args.concat(extras), (err, ...extras) => {
              if (err)
                return reject(err)
              resolve(extras)
            })
          })
        }
        args = args.concat(extras)
        if (done)
          done(...args)
        return args
      } catch (err) { throw err }
    }
    Object.defineProperty(this, method, { value: go })
    Object.defineProperty(this, 'use', { value: fn => stack.push(fn) })
  }
}

And here's a demo of my new version:

const mw = new Middleware()
mw.use((a, b, next) => {
  log('calling 1st func', { a, b })
  next()
})
mw.use((a, b, next) => {
  log('calling 2nd func', { a, b })
  next(null, 'c')
})
mw.use((a, b, c, next) => {
  log('calling 3rd func', { a, b, c })
  if (Math.random() > 0.5)
    next(new Error('uh oh'), c)
  else
    next(null, c)
})
mw.use((a, b, c, next) => {
  log('calling 4th func', { a, b, c })
  next(null, c)
})
try {
  let result = await mw.go('a', 'b', (a, b, c) => log('done', { a, b, c }))
  log(result)
} catch (err) {
  log('err', err.toString())
}

@kindziora
Copy link

thanks for the nice work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment