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, 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 { 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): 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): 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(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(staticClass: StaticClass | Instantiable, instance: T): this { return this.withoutParentScopes(() => super.registerSingletonInstance(staticClass, instance)) } /** * Register a given factory with the container. * @param {AbstractFactory} factory */ registerFactory(factory: AbstractFactory): 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(closure: () => T): T { const oldResolveParentScope = this.resolveParentScope this.resolveParentScope = false const value: T = closure() this.resolveParentScope = oldResolveParentScope return value } }