/**
* @module flitter-di/src/Container
*/
const MissingContainerDefinitionError = require('./MissingContainerDefinitionError')
/** Manages service definitions, instances, and deferred injection. */
class Container {
static TYPE_INJECTABLE = Symbol('injectable')
static TYPE_SINGLETON = Symbol('singleton')
/**
* Instantiates the container.
* @param {object} definitions - mapping of service name to static service CLASS definition
*/
constructor(definitions = {}) {
const def_map = {}
for ( const def_name in definitions ) {
if ( !definitions.hasOwnProperty(def_name) ) continue
def_map[def_name] = {
type: this.constructor.TYPE_INJECTABLE,
ref: definitions[def_name]
}
}
/**
* Static IoC item definitions from which instances are created or
* singleton values are returned when the items are requested.
* Should be mapping of item_name -> {type: Symbol, ref: *}.
* definition pairs.
* @type {object}
*/
this.definitions = def_map
/**
* Instantiated services. If a service has already been requested, it is
* stored here so that the single instance can be reused.
* @type {object}
*/
this.instances = {}
/**
* Already injected static service definitions. These are used to resolve
* circular dependencies.
* @type {object}
*/
this.statics = {}
/**
* Instance of the dependency injector this container is associated with.
* If this is specified, services will be injected with other services when
* they are instantiated.
* @type {boolean|module:flitter-di/src/DependencyInjector~DependencyInjector}
*/
this.di = false
/**
* Array of static class definitions with deferred services. These static
* definitions are waiting for a service to be registered with this container
* so it can be injected into the prototype and instances.
* @type {Array<*>}
*/
this.deferred_classes = []
}
/**
* Check if a service definition exists in this container.
* @param {string} service - the name of the service
* @returns {boolean} - true if the service definition exists in this container
*/
has(service) {
return !!this.definitions[service]
}
/**
* Get the container proxy. Allows accessing IoC items by name.
* @returns {{}}
*/
proxy() {
return new Proxy({}, {
get: (what, name) => {
return this.get(name)
}
})
}
/**
* Register a service class with the container. Allows the
* service to be requested and it will be instantiated and
* injected by the container.
* @param {string} service_name
* @param {typeof module:flitter-di/src/Service~Service} service_class - the uninstantiated Service class
*/
register_service(service_name, service_class) {
this.definitions[service_name] = {
type: this.constructor.TYPE_INJECTABLE,
ref: service_class
}
// check and process deferrals
if ( this.deferred_classes.length > 0 ) {
const deferred_requests = this.deferred_classes.map(x => x._di_deferred_services).flat()
if ( deferred_requests.includes(service_name) ) {
this._process_deferral(service_name, this.get(service_name))
}
}
}
/**
* Register an item as a singleton with the container.
* @param {string} singleton_name
* @param {*} value - the value tobe returned by the container
*/
register_singleton(singleton_name, value) {
this.definitions[singleton_name] = {
type: this.constructor.TYPE_SINGLETON,
ref: value,
}
// check and process deferrals
if ( this.deferred_classes.length > 0 ) {
const deferred_requests = this.deferred_classes.map(x => x._di_deferred_services).flat()
if ( deferred_requests.includes(singleton_name) ) {
this._process_deferral(singleton_name, this.get(singleton_name))
}
}
}
/**
* Fetch a container item by name. It it is an injectable item,
* it will be injected and instantiated before return.
* @param {string} name - the name of the IoC item
* @returns {module:flitter-di/src/Service~Service|*} - the service instance or singleton item
*/
get(name) {
const def = this.definitions[name]
if ( !def ) throw new MissingContainerDefinitionError(name)
// Return the singleton value, if applicable
if ( def.type === this.constructor.TYPE_SINGLETON ) {
return def.ref
} else if ( def.type === this.constructor.TYPE_INJECTABLE ) {
// Store the static reference first.
// This allows us to resolve circular dependencies.
if ( !this.statics[name] ) {
this.statics[name] = def.ref
if ( this.di ) {
this.di.make(this.statics[name])
}
}
if ( !this.instances[name] ) {
const ServiceClass = this.statics[name]
this.instances[name] = new ServiceClass()
}
return this.instances[name]
}
}
/**
* Fetch a container item by name.
* @deprecated Please use Container.get from now on. This will be removed in the future.
* @param {string} name
* @returns {module:flitter-di/src/Service~Service|*}
*/
service(name) {
return this.get(name)
}
/**
* Process deferred classes that need the provided service name and instance.
* @param {string} item_name - the referential name of the IoC item
* @param {module:flitter-di/src/Service~Service|*} item - the Service or item to be injected
* @private
*/
_process_deferral(item_name, item) {
const new_deferrals = []
for ( const Class of this.deferred_classes ) {
if ( Class._di_deferred_services.includes(item_name) ) {
Class.__deferral_callback(item_name, item)
}
if ( Class.__has_deferred_services ) {
new_deferrals.push(Class)
}
}
this.deferred_classes = new_deferrals
}
/**
* Defer a static class to have its missing IoC items filled in as they
* become available in the service container. The class should extend
* from Injectable.
* @param {*} Class - the static class to be deferred
*/
defer(Class) {
if ( !this.__is_deferrable(Class) ) {
throw new TypeError('Cannot defer non-deferrable class: '+Class.name)
}
this.deferred_classes.push(Class)
}
/**
* Checks if a class is deferrable. That is, does it have the requirements
* for functioning with the defer logic. In almost all cases, these should be
* satisfied by having the Class extend from Injectable.
* @param {*} Class - the static class to check
* @returns {boolean} - true if the class is deferrable
* @private
*/
__is_deferrable(Class) {
return (
Array.isArray(Class._di_deferred_services)
&& Array.isArray(Class._di_deferred_instances)
&& '_di_allow_defer' in Class
&& typeof Class.__deferral_callback === 'function'
&& '__has_deferred_services' in Class
)
}
}
module.exports = exports = Container