|
|
@ -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) |
|
|
|