You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
5.8 KiB
169 lines
5.8 KiB
/** Manages service definitions, instances, and deferred injection. */
|
|
class Container {
|
|
constructor(definitions = {}) {
|
|
|
|
/**
|
|
* Static service definitions from which instances are created when
|
|
* the services are requested. Should be in service name -> service
|
|
* definition pairs.
|
|
* @type object
|
|
*/
|
|
this.definitions = definitions
|
|
|
|
/**
|
|
* 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|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 {*[]}
|
|
*/
|
|
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]
|
|
}
|
|
|
|
/**
|
|
* 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 {Service|*} - the service instance of service container proxy
|
|
*/
|
|
service(service = false) {
|
|
if ( service === false ) {
|
|
return new Proxy({}, {
|
|
get: (obj, prop) => {
|
|
return this.service(prop)
|
|
}
|
|
})
|
|
}
|
|
|
|
if ( !this.definitions[service] ) {
|
|
throw new Error('No such service registered with this container: '+service)
|
|
}
|
|
|
|
// 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])
|
|
}
|
|
}
|
|
|
|
if ( !this.instances[service] ) {
|
|
const ServiceClass = this.statics[service]
|
|
this.instances[service] = new ServiceClass()
|
|
}
|
|
|
|
return this.instances[service]
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
register(service_name, service_class) {
|
|
this.definitions[service_name] = service_class
|
|
this._process_deferral(service_name, this.service(service_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 {Service} service_instance - the service class instance
|
|
*/
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* Process deferred classes that need the provided service name and instance.
|
|
* @param {string} service_name - the referential name of the service
|
|
* @param {Service} service_instance - the instance of the service
|
|
* @private
|
|
*/
|
|
_process_deferral(service_name, service_instance) {
|
|
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.__has_deferred_services ) {
|
|
new_deferrals.push(Class)
|
|
}
|
|
}
|
|
|
|
this.deferred_classes = new_deferrals
|
|
}
|
|
|
|
/**
|
|
* Defer a static class to have its missing services 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)
|
|
}
|
|
|
|
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
|