const R = require('ramda')
module.exports = (obj, { before, after } = { before: R.always(), after: R.always() }) => {
const methods = R.mapObjIndexed((x, method) => {
if (typeof x !== 'function') {
return x
}
return (...params) => {
before({ method, params })
const result = x(...params)
// async
if (result && result.constructor === Promise) {
return result.then(y => { after({ method, params, result: y }); return y })
}
// sync
after({ method, params, result })
return result
}
}, obj)
return { ...methods }
}
module.exports = factory => services => (...params) => ({
// pass through all parent methods
...factory(services)(...params),
// add our own methods
speak (msg) {
console.log(msg)
},
})
const assert = require('assert')
const wrapMethods = require('./wrapMethods')
module.exports = ({ level } = { level: 'info' }) => factory => services => (...params) => {
const parent = factory(services)(...params)
const { logger } = services
assert(logger, 'services.logger is not defined')
const methods = wrapMethods(parent, {
before ({ method, params }) {
logger[level]({ method, params }, 'calling method')
},
after ({ method, params, result }) {
logger[level]({ method, params, result }, 'method result')
},
})
return {
...methods
}
}
const Locks = require('./locks')
const wrapMethods = require('./wrapMethods')
module.exports = factory => services => (...params) => {
const parent = factory(services)(...params)
const locks = Locks()
const methods = wrapMethods(parent, {
before ({ method }) {
locks.lock(method)
},
after ({ method }) {
locks.unlock(method)
},
})
return {
...methods,
...locks
}
}
module.exports = () => ({
_locks: {},
_set (key, value) {
this._locks[key] = value
},
lock (key) {
this.assert(key, false)
this._set(key, true)
},
unlock (key) {
this.assert(key, true)
this._set(key, false)
},
assert (key, value) {
const state = this._locks[key] || false
if (state !== value) {
const verb = value ? 'add' : 'remove'
const status = value ? 'already' : 'not yet'
const message = `cannot ${verb} lock on method '${key}'; method is ${status} locked`
throw new Error(message)
}
},
})
const R = require('ramda')
const { Logger } = require('../logger')
// higher order "plugin" factories; these functions accept
// a factory, add functionality to it, and then return it
const locksHof = require('./locksHof')
const loggerHof = require('./methodLoggerHof')
const speakHof = require('./speakHof')
// create a "base" higher-order factory that has adds the methods
// from all 3 higher order factories (locks, log, speak)
const baseFactoryHof = R.compose(
// locksHof adds lock flags to all instance methods so that the same
// method cannot run twice concurrently
// we did not make this configurable, so we don't pass anything in
locksHof,
// loggerHof will cause every method to log its params and results
// this hof is configurable; we tell it what level to log at
loggerHof({ level: 'info' }),
// speakHof will add a .speak() method
speakHof,
)
// create a factory
const MathFactory = services => ({ someOption } = { someOption: 3 }) => ({
getValue: (val = 0) => new Promise(resolve => setTimeout(resolve, 10, val + someOption))
})
// create a logger dependency
const logger = new Logger('factories', { level: 'info' })
;(async () => {
// extend the factory with the baseFactoryHof
const MathFactoryExtended = baseFactoryHof(MathFactory)
// call the factory to get the interface
// with a class, we would do new MyService(services, options)
// since factories are functions, we do: MyFactory(services)(options)
const services = { logger }
const options = { someOption: 1 }
const math = MathFactoryExtended(services)(options)
// now we have the extended factory
const result = await math.getValue(1)
// using functionality from speakHof
math.speak(result)
// using functionality from locksHof
// math.lock('getValue')
await math.getValue()
})()