Browse Source

Start proper IoC refactor

tags/ci-01
garrettmills 6 months ago
parent
commit
780f2534ef
No known key found for this signature in database GPG Key ID: 6ACD58D6ADACFC6E
5 changed files with 171 additions and 62 deletions
  1. +99
    -52
      src/Container.js
  2. +34
    -8
      src/DependencyInjector.js
  3. +2
    -2
      src/Injectable.js
  4. +7
    -0
      src/MissingContainerDefinitionError.js
  5. +29
    -0
      test.js

+ 99
- 52
src/Container.js View File

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


+ 34
- 8
src/DependencyInjector.js View File

@@ -2,6 +2,9 @@
* @module flitter-di/src/DependencyInjector
*/

/**
* @type {typeof module:flitter-di/src/Container~Container}
*/
const Container = require('./Container')

/** Manages services and injects classes from its service container. */
@@ -16,14 +19,27 @@ class DependencyInjector {
}

/**
* Inject a static class with the services it requests. This mutates
* the class' prototype.
* @param Class
* Instantiate the passed in class. If it is injectable, it will be injected.
* @param {typeof module:flitter-di/src/Injectable~Injectable} Class
* @param {...*} args - additional arguments to be passed to the constructor of the class
* @returns {*} - the injected static reference to the Class
*/
make(Class) {
make(Class, ...args) {
if ( this.__is_injectable(Class) ) {
this.inject(Class)
}

return new Class(...args)
}

/**
* Inject the passed in Class with the IoC items it requires.
* @param {typeof module:flitter-di/src/Injectable~Injectable} Class
* @returns {*}
*/
inject(Class) {
if ( !this.__is_injectable(Class) ) {
throw new Error('Cannot inject non-injectable class: '+Class.name)
throw new TypeError(`Cannot inject non-injectable class: ${Class.name}`)
}

Class.__inject(this.container)
@@ -34,11 +50,21 @@ class DependencyInjector {
* 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
* @deprecated - prefer DependencyInjector.get. This will be removed in the future.
* @param {string} name - the name of the service
* @returns {module:flitter-di/src/Service~Service|undefined|Proxy} - the service instance or service container proxy
*/
service(name) {
return this.container.service(name)
return this.container.get(name)
}

/**
* Fetch an IoC item by name.
* @param {string} name
* @returns {*}
*/
get(name) {
return this.container.get(name)
}

/**
@@ -68,7 +94,7 @@ class DependencyInjector {

Module.prototype.require = function() {
const value = original_require.apply(this, arguments)
return di.__is_injectable(value) ? di.make(value) : value
return di.__is_injectable(value) ? di.inject(value) : value
}
}



+ 2
- 2
src/Injectable.js View File

@@ -58,11 +58,11 @@ class Injectable {
this.services.forEach(name => {
if ( !this._di_allow_defer ) {
// If this class' services aren't deferrable, then just fetch their instances
this.prototype[name] = container.service(name)
this.prototype[name] = container.get(name)
} else {
// Otherwise, grab them if they exist, or put in deferral requests
if ( container.has(name) ) {
this.prototype[name] = container.service(name)
this.prototype[name] = container.get(name)
} else {
this._di_deferred_services.push(name)
}


+ 7
- 0
src/MissingContainerDefinitionError.js View File

@@ -0,0 +1,7 @@
class MissingContainerDefinitionError extends TypeError {
constructor(item_name) {
super(`An item with the name "${item_name}" could not be found in the DI Container.`)
}
}

module.exports = exports = MissingContainerDefinitionError

+ 29
- 0
test.js View File

@@ -0,0 +1,29 @@
const { Container, DependencyInjector, Service } = require("./index")

class ServiceA extends Service {
num = 1.34
}

class ServiceB extends Service {
static get services() {
return ['service_a', 'num_pi']
}

get num() {
return this.service_a.num * this.num_pi
}
}

const ioc_container = new Container({
service_a: ServiceA,
})

ioc_container.register_singleton('num_pi', 3.141)
ioc_container.register_service('service_b', ServiceB)

const ioc_di = new DependencyInjector(ioc_container)

global.cont = ioc_container
global.di = ioc_di
global.AA = ServiceA
global.BB = ServiceB

Loading…
Cancel
Save