Skip to content

Instantly share code, notes, and snippets.

@dschnare
Last active September 21, 2019 00:45
Show Gist options
  • Save dschnare/1d20405f988bf8ec344734c26fea50ef to your computer and use it in GitHub Desktop.
Save dschnare/1d20405f988bf8ec344734c26fea50ef to your computer and use it in GitHub Desktop.
Simple service container
/**
* A simple service container.
*
* @example
* const app = express()
*
* const appContainer = new ServiceContainer()
* const config = Object.freeze({ })
* appContainer.constant('Config', config)
* appContainer.singleton('Db', () => new Db())
* appContainer.scoped('UserService', c => new UserService(c.resolve('Db'))
*
* // Add child service container to the locals for the response
* app.use((req, res, next) => {
* try {
* const container = new ServiceContainer(appContainer)
* res.locals.container = container
* res.once('finish', () => {
* res.locals.container = null
* container.dispose().catch(error => console.error(error))
* })
* } catch (error) {
* next(error)
* }
* })
*
* app.get('/', (req, res) => {
*
* })
*/
class ServiceContainer {
/**
* @param {ServiceContainer} [parent]
*/
constructor (parent = null) {
this._parent = parent
this._services = new Map()
}
/**
* Binds services and/or function arguments to a function.
*
* @example
* const container = new ServiceContainer()
* container.transient('UserService', () => new UserService())
* const getUserById = (UserService, id) => UserService.getUserById(id)
* const getAUser = container.bind(getUserById, ['UserService'])
* @param {(...args) => any} fn
* @param {any[]} bindings
*/
bind (fn, bindings) {
return (...args) => {
return this.invoke(fn, bindings.concat(args))
}
}
/**
* Invokes a function with service and/or function arguments.
*
* @example
* const container = new ServiceContainer()
* container.transient('UserService', () => new UserService())
* const getUserById = (UserService, id) => UserService.getUserById(id)
* const user = await container.invoke(getUserById, ['UserService', 45])
* @param {(...args) => any} fn
* @param {any[]} bindings
*/
invoke (fn, bindings = []) {
const args = bindings.map(bn => {
if (typeof bn === 'string') {
return this.resolve(bn)
} else {
return bn(this)
}
})
return fn(...args)
}
/**
* Attempts to resolve a service by name from the container.
*
* Any extra arguments provided will be passed to the service factory function
* if called.
*
* @param {string} serviceName
* @param {...any} args
*/
resolve (serviceName, ...args) {
// This container has the service registered.
if (this._services.has(serviceName)) {
const entry = this._services.get(serviceName)
if (entry.inst) return entry.inst
const inst = entry.factory(...[ this, ...args ])
// If the entry lifetime is transient then it does not get saved
if (entry.lifetime !== 'transient') entry.inst = inst
return inst
// The parent container has the service registered.
} else if (this._parent && this._parent._services.has(serviceName)) {
let entry = this._parent._services.get(serviceName)
// If the service lifetime is scoped then we save the instance on this
// container's service map (i.e. the child container).
if (entry.lifetime === 'scoped') {
entry = { ...entry }
entry.inst = entry.factory(...[ this, ...args ])
this._services.set(serviceName, entry)
return entry.inst
// Otherwise create the service instance, but save the instance on the
// parent service container's entry record.
} else {
entry.inst = entry.factory(...[ this, ...args ])
return entry.inst
}
// No service is registered.
} else {
throw new Error('Failed to resolve service. (' + serviceName + ')')
}
}
/**
* Registers a service value.
*
* @param {string} serviceName The service name
* @param {any} value The service value
*/
constant (serviceName, value) {
this._services.set(serviceName, { factory: () => value, inst: value, lifetime: 'constant' })
}
/**
* Registers a singleton service whereby the factory function will only be
* called the first time the service is resolved.
*
* @param {string} serviceName The service name
* @param {{ (c: ServiceContainer, ...args: any[]): any }} factory Service factory
*/
singleton (serviceName, factory) {
this._services.set(serviceName, { factory, inst: null, lifetime: 'singleton' })
}
/**
* Registers a transient service whereby the factory function will be
* called every time the service is resolved.
*
* @param {string} serviceName The service name
* @param {{ (c: ServiceContainer, ...args: any[]): any }} factory Service factory
*/
transient (serviceName, factory) {
this._services.set(serviceName, { factory, inst: null, lifetime: 'transient' })
}
/**
* Registers a scoped singleton service whereby the factory function will only
* be called the first time the service is resolved.
*
* If the container that resolves the scoped service is not a *child* container
* then the service has singleton lifetime. Otherwise the service will be
* disposed when the *child* container is disposed.
*
* @example
* const parent = new ServiceContainer()
* const child = new ServiceContainer(parent)
* parent.singleton('Db', () => new Db())
* parent.scoped('UserService', c => new UserService(c.resolve('Db')))
* const userService = child.resolve('UserService')
* await child.dispose() // userService is disposed
* @param {string} serviceName The service name
* @param {{ (c: ServiceContainer, ...args: any[]): any }} factory Service factory
*/
scoped (serviceName, factory) {
this._services.set(serviceName, { factory, inst: null, lifetime: 'scoped' })
}
/**
* Dispose a single service instance or all service instances and clears all
* parent-child related references.
*
* This will call the `dispose()` method on any service instance being disposed.
*
* @param {string} [serviceName] The service to dispose
* @return {Promise<void>} Resolved when all services are disposed successfully
*/
dispose (serviceName = null) {
const serviceNames = serviceName
? [ serviceName ]
: [ ...this._services.keys() ]
return this._disposeServices(serviceNames).then(() => {
this._parent = null
}, error => {
this._parent = null
throw error
})
}
_disposeServices (serviceNames) {
return Promise.all(
serviceNames.map(serviceName => {
const entry = this._services.get(serviceName)
const inst = entry.inst
entry.inst = null
if (inst && typeof inst.dispose === 'function') {
return inst.dispose()
}
})
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment