Refactor event bus and queue system; detect cycles in DI realization and make
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2022-01-26 19:37:54 -06:00
parent 506fb55c74
commit 6d1cf18680
69 changed files with 1673 additions and 720 deletions

View File

@@ -8,7 +8,7 @@ import {
TypedDependencyKey,
} from './types'
import {AbstractFactory} from './factory/AbstractFactory'
import {collect, Collection, globalRegistry, logIfDebugging} from '../util'
import {collect, Collection, ErrorWithContext, globalRegistry, logIfDebugging} from '../util'
import {Factory} from './factory/Factory'
import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError'
import {ClosureFactory} from './factory/ClosureFactory'
@@ -25,6 +25,27 @@ export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resol
* A container of resolve-able dependencies that are created via inversion-of-control.
*/
export class Container {
/**
* Set to true when we're realizing a container.
* Used to prevent infinite recursion when `getContainer()` is accidentally called
* from somewhere within the `realizeContainer()` call.
*/
protected static realizingContainer = false
/**
* List of dependency keys currently being `make`'d as a reverse stack.
* This is used to detect dependency cycles.
* @protected
*/
protected static makeStack?: Collection<DependencyKey>
/**
* The 100 most recent dependency keys that were `make`'d. Used to help with
* debugging cyclic dependency errors.
* @protected
*/
protected static makeHistory?: Collection<DependencyKey>
/**
* Given a Container instance, apply the ContainerBlueprint to it.
* @param container
@@ -34,6 +55,10 @@ export class Container {
.resolve()
.map(factory => container.registerFactory(factory))
ContainerBlueprint.getContainerBlueprint()
.resolveConstructable()
.map((factory: StaticClass<AbstractFactory<any>, any>) => console.log(factory)) // eslint-disable-line no-console
ContainerBlueprint.getContainerBlueprint()
.resolveConstructable()
.map((factory: StaticClass<AbstractFactory<any>, any>) => container.registerFactory(container.make(factory)))
@@ -54,8 +79,14 @@ export class Container {
public static getContainer(): Container {
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
if ( !existing ) {
if ( this.realizingContainer ) {
throw new ErrorWithContext('Attempted getContainer call during container realization.')
}
this.realizingContainer = true
const container = Container.realizeContainer(new Container())
globalRegistry.setGlobal('extollo/injector', container)
this.realizingContainer = false
return container
}
@@ -403,13 +434,92 @@ export class Container {
* @param {...any} parameters
*/
make<T>(target: DependencyKey, ...parameters: any[]): T {
if ( this.hasKey(target) ) {
return this.resolveAndCreate(target, ...parameters)
} else if ( typeof target !== 'string' && isInstantiable(target) ) {
return this.produceFactory(new Factory(target), parameters)
} else {
throw new TypeError(`Invalid or unknown make target: ${target}`)
if ( !Container.makeStack ) {
Container.makeStack = new Collection()
}
if ( !Container.makeHistory ) {
Container.makeHistory = new Collection()
}
Container.makeStack.push(target)
if ( Container.makeHistory.length > 100 ) {
Container.makeHistory = Container.makeHistory.slice(1, 100)
}
Container.makeHistory.push(target)
this.checkForMakeCycles()
if ( this.hasKey(target) ) {
const realized = this.resolveAndCreate(target, ...parameters)
Container.makeStack.pop()
return realized
} else if ( typeof target !== 'string' && isInstantiable(target) ) {
const realized = this.produceFactory(new Factory(target), parameters)
Container.makeStack.pop()
return realized
}
Container.makeStack.pop()
throw new TypeError(`Invalid or unknown make target: ${target}`)
}
/**
* Check the `makeStack` for duplicates and throw an error if a dependency cycle is
* detected. This is used to prevent infinite mutual recursion when cyclic dependencies
* occur.
* @protected
*/
protected checkForMakeCycles(): void {
if ( !Container.makeStack ) {
Container.makeStack = new Collection()
}
if ( !Container.makeHistory ) {
Container.makeHistory = new Collection()
}
if ( Container.makeStack.unique().length !== Container.makeStack.length ) {
const displayKey = (key: DependencyKey) => {
if ( typeof key === 'string' ) {
return 'key: `' + key + '`'
} else {
return `key: ${key.name}`
}
}
const makeStack = Container.makeStack
.reverse()
.map(displayKey)
const makeHistory = Container.makeHistory
.reverse()
.map(displayKey)
console.error('Make Stack:') // eslint-disable-line no-console
console.error(makeStack.join('\n')) // eslint-disable-line no-console
console.error('Make History:') // eslint-disable-line no-console
console.error(makeHistory.join('\n')) // eslint-disable-line no-console
throw new ErrorWithContext('Cyclic dependency chain detected', {
makeStack,
makeHistory,
})
}
}
/**
* Create a new instance of the dependency key using this container, ignoring any pre-existing instances
* in this container.
* @param key
* @param parameters
*/
makeNew<T>(key: TypedDependencyKey<T>, ...parameters: any[]): T {
if ( isInstantiable(key) ) {
return this.produceFactory(new Factory(key), parameters)
}
throw new TypeError(`Invalid or unknown make target: ${key}`)
}
/**