Last active
September 21, 2019 00:45
-
-
Save dschnare/1d20405f988bf8ec344734c26fea50ef to your computer and use it in GitHub Desktop.
Simple service container
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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