add deferred injection

develop
garrettmills 4 years ago
parent 3eb82edf9c
commit bd29ba26a0

@ -100,6 +100,46 @@ service.a.foo = "Bar" // A { foo: "Bar" }
service.a.b.a.b.a.b.a.foo // "Bar"
```
### Deferred Services
Sometimes, it may be that services don't always load in an order that ensures that all injectable classes are loaded after the services themselves. In these cases, Flitter DI will defer the injection of the missing services until they are registered with the service container. When they are registered, the class will be injected with those services, as well as any instances of the class that were created before the service was registered. A basic example:
```javascript
const { Service, Injectable, DependencyInjector } = require('flitter-di')
class A extends Service {
number = 3.141
}
class B extends Service {
number = (22/7)
}
class MyClass extends Injectable {
static get services() {
return ['a', 'b']
}
}
const di = new DependencyInjector()
di.register('a', A)
di.make(MyClass)
const my_class = new MyClass()
console.log(my_class.a) // => A { number: 3.141 }
console.log(my_class.b) // => B { }
di.register('b', B)
console.log(my_class.a) // => A { number: 3.141 }
console.log(my_class.b) // => B { number: 3.142 }
// Note that you can disable deferred injection like so:
class BuzzKill extends Injectable {
static _di_allow_defer = false
}
```
## License
Copyright 2019 Garrett Mills

@ -1,15 +1,62 @@
/** 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({}, {
@ -20,14 +67,16 @@ class Container {
}
if ( !this.definitions[service] ) {
throw new Error('No such service registered with this container.')
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]
this.di.make(this.statics[service])
if ( this.di ) {
this.di.make(this.statics[service])
}
}
if ( !this.instances[service] ) {
@ -38,19 +87,80 @@ class Container {
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)
}
__is_injectable(Class) {
console.log('WARNING: use of flitter-di/Container#__is_injectable is deprecated and will be removed in the future')
/**
* 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 (
Class.services
&& Array.isArray(Class.services)
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
)
}
}

@ -1,23 +1,47 @@
const Container = require('./Container')
/** Manages services and injects classes from its service container. */
class DependencyInjector {
constructor(container = new Container()) {
/**
* The service container used by this dependency injector.
* @type {Container}
*/
this.container = container
this.container.di = this
}
/**
* Inject a static class with the services it requests. This mutates
* the class' prototype.
* @param Class
* @returns {*} - the injected static reference to the Class
*/
make(Class) {
if ( !this.__is_injectable(Class) ) {
throw new Error('Cannot inject non-injectable class.')
throw new Error('Cannot inject non-injectable class: '+Class.name)
}
Class.__inject(this.container)
return Class
}
/**
* 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 {{}|*} - the service instance of service container proxy
*/
service(name) {
return this.container.service(name)
}
/**
* Verify that the injector's container has a service or set of services.
* @param {string|string[]} name - service name or array of service names
* @returns {boolean} - true if the container has the service(s)
*/
has(name) {
if ( Array.isArray(name) ) {
return name.some(x => this.container.has(x))
@ -26,6 +50,14 @@ class DependencyInjector {
return this.container.has(name)
}
/**
* Verify that a class is injectable. This means that it has a static __inject
* method and that method takes at least one argument. In almost all cases, this
* should be satisfied by using the Injectable base class.
* @param {*} Class - the class to check
* @returns {boolean} - true if the class is injectable
* @private
*/
__is_injectable(Class) {
return (
typeof Class.__inject === 'function'

@ -1,12 +1,103 @@
/** Base class for classes that support service injection. */
class Injectable {
/**
* If true, the injector will defer the class if the class
* requests any services that the container is missing. These
* services are filled in later and added to the prototype and
* any instances. True by default.
* @type {boolean}
* @private
*/
static _di_allow_defer = true
/**
* List of services that were deferred and not provided at the time of injection.
* @type {string[]}
* @private
*/
static _di_deferred_services = []
/**
* Collection of instances of this class that need to have the deferred service
* instances injected into them when the deferred services are finally provided.
* @type {*[]}
* @private
*/
static _di_deferred_instances = []
/**
* Get the names of services required by this class.
* @returns {string[]}
*/
static get services() {
return []
}
/**
* Checks if the class has any missing deferred services.
* @returns {boolean} - true if there are deferred services
* @private
*/
static get __has_deferred_services() {
return this._di_deferred_services.length > 0
}
/**
* Inject the class with services from the specified container. These
* services are injected directly into this class' prototype. If deferral
* is enabled, services not in the container will be stored and the class
* will be deferred.
* @param {Container} container
* @private
*/
static __inject(container) {
this.services.forEach(name => {
this.prototype[name] = container.service(name)
if ( !this._di_allow_defer ) {
// If this class' services aren't deferrable, then just fetch their instances
this.prototype[name] = container.service(name)
} else {
// Otherwise, grab them if they exist, or put in deferral requests
if ( container.has(name) ) {
this.prototype[name] = container.service(name)
} else {
this._di_deferred_services.push(name)
}
}
})
if ( this._di_allow_defer && this.__has_deferred_services ) {
container.defer(this)
}
}
/**
* Called when a deferred service is registered with the container.
* Injects the missing service into the prototype and any instances of this
* class.
* @param {string} service_name - the deferred service name
* @param {Service} service_instance - the instance of the service
* @private
*/
static __deferral_callback(service_name, service_instance) {
if ( this._di_deferred_services.includes(service_name) ) {
for ( const instance of this._di_deferred_instances ) {
instance[service_name] = service_instance
}
this._di_deferred_services = this._di_deferred_services.filter(x => x !== service_name)
}
}
/**
* Instantiates the class. If deferral is enabled and the static class
* has deferred services, register this instance as needing to be injected
* with those services when they become available.
*/
constructor() {
const Class = this.constructor
if ( Class._di_allow_defer && Class.__has_deferred_services ) {
Class._di_deferred_instances.push(this)
}
}
}

@ -1,4 +1,5 @@
const Injectable = require('./Injectable')
/** A service that can be registered with a container and used in a single-instance format. */
class Service extends Injectable {
}

Loading…
Cancel
Save