rjhilgefort
10/12/2018 - 10:17 PM

Factory Patter & Higher Order Factories

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