lib/src/di/ScopedContainer.ts
garrettmills f00233d49a
All checks were successful
continuous-integration/drone/push Build is passing
Add middleware and logic for bootstrapping the session auth
2021-06-05 13:24:12 -05:00

142 lines
5.0 KiB
TypeScript

import {Container, MaybeDependency, MaybeFactory} from './Container'
import {DependencyKey, Instantiable, StaticClass} from './types'
import {AbstractFactory} from './factory/AbstractFactory'
/**
* A container that uses some parent container as a base, but
* can have other factories distinct from that parent.
*
* If an instance is not found in this container, it will be resolved from
* the parent container.
*
* However, if an instance IS found in this container, it will ALWAYS be
* resolved from this container, rather than the parent.
*
* This can be used to create scope-specific containers that can still resolve
* the global dependencies, while keeping scope-specific dependencies separate.
*
* @example
* The Request class from @extollo/lib is a ScopedContainer. It can resolve
* all dependencies that exist in the global Container, but it can also have
* request-specific services (like the Session) injected into it.
*
* @extends Container
*/
export class ScopedContainer extends Container {
/**
* Create a new scoped container based on a parent container instance.
* @param container
*/
public static fromParent(container: Container): ScopedContainer {
return new ScopedContainer(container)
}
private resolveParentScope = true
constructor(
private parentContainer: Container,
) {
super()
this.registerSingletonInstance<ScopedContainer>(ScopedContainer, this)
}
hasInstance(key: DependencyKey): boolean {
return super.hasInstance(key) || (this.resolveParentScope && this.parentContainer.hasInstance(key))
}
hasKey(key: DependencyKey): boolean {
return super.hasKey(key) || (this.resolveParentScope && this.parentContainer.hasKey(key))
}
getExistingInstance(key: DependencyKey): MaybeDependency {
const inst = super.getExistingInstance(key)
if ( inst ) {
return inst
}
if ( this.resolveParentScope ) {
return this.parentContainer.getExistingInstance(key)
}
}
resolve(key: DependencyKey): MaybeFactory<any> {
const factory = super.resolve(key)
if ( factory ) {
return factory
}
if ( this.resolveParentScope ) {
return this.parentContainer.resolve(key)
}
}
/**
* Register a basic instantiable class as a standard Factory with this container.
* @param {Instantiable} dependency
*/
register(dependency: Instantiable<any>): this {
return this.withoutParentScopes(() => super.register(dependency))
}
/**
* Register the given function as a factory within the container.
* @param {string} name - unique name to identify the factory in the container
* @param {function} producer - factory to produce a value
*/
registerProducer(name: DependencyKey, producer: () => any): this {
return this.withoutParentScopes(() => super.registerProducer(name, producer))
}
/**
* Register a basic instantiable class as a standard Factory with this container,
* identified by a string name rather than static class.
* @param {string} name - unique name to identify the factory in the container
* @param {Instantiable} dependency
*/
registerNamed(name: string, dependency: Instantiable<any>): this {
return this.withoutParentScopes(() => super.registerNamed(name, dependency))
}
/**
* Register a value as a singleton in the container. It will not be instantiated, but
* can be injected by its unique name.
* @param {string} key - unique name to identify the singleton in the container
* @param value
*/
registerSingleton<T>(key: DependencyKey, value: T): this {
return this.withoutParentScopes(() => super.registerSingleton(key, value))
}
/**
* Register a static class to the container along with its already-instantiated
* instance that will be used to resolve the class.
* @param staticClass
* @param instance
*/
registerSingletonInstance<T>(staticClass: StaticClass<T, any> | Instantiable<T>, instance: T): this {
return this.withoutParentScopes(() => super.registerSingletonInstance(staticClass, instance))
}
/**
* Register a given factory with the container.
* @param {AbstractFactory} factory
*/
registerFactory(factory: AbstractFactory<unknown>): this {
return this.withoutParentScopes(() => super.registerFactory(factory))
}
/**
* Execute a closure on this container, disabling parent-resolution.
* Effectively, the closure will have access to this container as if
* it were NOT a scoped container, and only contained its factories.
* @param closure
*/
withoutParentScopes<T>(closure: () => T): T {
const oldResolveParentScope = this.resolveParentScope
this.resolveParentScope = false
const value: T = closure()
this.resolveParentScope = oldResolveParentScope
return value
}
}