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
})
@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