|
|
|
@ -2,21 +2,35 @@
|
|
|
|
|
* @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 service definitions from which instances are created when
|
|
|
|
|
* the services are requested. Should be in service name -> service
|
|
|
|
|
* 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 = definitions
|
|
|
|
|
this.definitions = def_map
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Instantiated services. If a service has already been requested, it is
|
|
|
|
@ -59,79 +73,112 @@ class Container {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetch a service by name. If no name is provided, return the service
|
|
|
|
|
* proxy container. This container has getters for all the services by
|
|
|
|
|
* name.
|
|
|
|
|
* @param {string} service - the name of the service
|
|
|
|
|
* @returns {module:flitter-di/src/Service~Service|undefined} - the service instance or service container proxy
|
|
|
|
|
* Get the container proxy. Allows accessing IoC items by name.
|
|
|
|
|
* @returns {{}}
|
|
|
|
|
*/
|
|
|
|
|
service(service = false) {
|
|
|
|
|
if ( service === false ) return this.proxy()
|
|
|
|
|
proxy() {
|
|
|
|
|
return new Proxy({}, {
|
|
|
|
|
get: (what, name) => {
|
|
|
|
|
return this.get(name)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !this.definitions[service] ) {
|
|
|
|
|
throw new Error('No such service registered with this container: '+service)
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store the static reference first.
|
|
|
|
|
// This allows us to resolve circular dependencies.
|
|
|
|
|
if ( !this.statics[service] ) {
|
|
|
|
|
this.statics[service] = this.definitions[service]
|
|
|
|
|
if ( this.di ) {
|
|
|
|
|
this.di.make(this.statics[service])
|
|
|
|
|
// 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))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !this.instances[service] ) {
|
|
|
|
|
const ServiceClass = this.statics[service]
|
|
|
|
|
this.instances[service] = new ServiceClass()
|
|
|
|
|
/**
|
|
|
|
|
* 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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.instances[service]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
proxy() {
|
|
|
|
|
return new Proxy({}, {
|
|
|
|
|
get: (what, name) => {
|
|
|
|
|
return this.service(name)
|
|
|
|
|
// 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))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Register a class definition as a service. When requested, the service
|
|
|
|
|
* for this class will be created from the class' instance.
|
|
|
|
|
* @param {string} service_name - the referential name of the service
|
|
|
|
|
* @param {*} service_class - the service class definition
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
register(service_name, service_class) {
|
|
|
|
|
this.definitions[service_name] = service_class
|
|
|
|
|
this._process_deferral(service_name, this.service(service_name))
|
|
|
|
|
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]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Register a class instance as a service. When requested, the provided
|
|
|
|
|
* instance will be returned as the instance of the service. The instance's
|
|
|
|
|
* constructor is saved as the service definition.
|
|
|
|
|
* @param {string} service_name - the referential name of the service
|
|
|
|
|
* @param {module:flitter-di/src/Service~Service} service_instance - the service class instance
|
|
|
|
|
* 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|*}
|
|
|
|
|
*/
|
|
|
|
|
register_as_instance(service_name, service_instance) {
|
|
|
|
|
this.definitions[service_name] = service_instance.constructor
|
|
|
|
|
this.instances[service_name] = service_instance
|
|
|
|
|
this._process_deferral(service_name, service_instance)
|
|
|
|
|
service(name) {
|
|
|
|
|
return this.get(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Process deferred classes that need the provided service name and instance.
|
|
|
|
|
* @param {string} service_name - the referential name of the service
|
|
|
|
|
* @param {module:flitter-di/src/Service~Service} service_instance - the instance of the service
|
|
|
|
|
* @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(service_name, service_instance) {
|
|
|
|
|
_process_deferral(item_name, item) {
|
|
|
|
|
const new_deferrals = []
|
|
|
|
|
for ( const Class of this.deferred_classes ) {
|
|
|
|
|
if ( Class._di_deferred_services.includes(service_name) ) {
|
|
|
|
|
Class.__deferral_callback(service_name, service_instance)
|
|
|
|
|
if ( Class._di_deferred_services.includes(item_name) ) {
|
|
|
|
|
Class.__deferral_callback(item_name, item)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( Class.__has_deferred_services ) {
|
|
|
|
@ -143,14 +190,14 @@ class Container {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Defer a static class to have its missing services filled in as they
|
|
|
|
|
* 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 Error('Cannot defer non-deferrable class: '+Class.name)
|
|
|
|
|
throw new TypeError('Cannot defer non-deferrable class: '+Class.name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.deferred_classes.push(Class)
|
|
|
|
|