UNPKG

@mooncake/container

Version:

DI(dependency injection) container for JavaScript and TypeScript.

273 lines (257 loc) 8.63 kB
import { AsyncHookMap } from 'async-hooks-map'; import { getBindActions, getInjectMetas } from './decorators'; import { AliasRegistion, ClassRegistion, FactoryRegistion, Registion, ValueRegistion } from './registion'; import { BindOption, Constructor, ContainerBinder, ContainerResolver, Factory, ID, InjectError } from './types'; export class Container implements ContainerResolver, ContainerBinder { protected _map = new AsyncHookMap<any, Registion<any>>() protected _lazyBinds: { [scope: string]: Map<any, Registion<any>> } = {} protected _autoBinds: Set<Function> = new Set() /** * alias of bindValue * * @template T * @param {ID<T>} id * @param {T} value * @memberof ContainerClass */ set<T> (id: ID<T>, value: T, opt: { scope?: string } = {}): this { return this.bindValue(id, value, opt) } /** * bind with value, it will be singleton * * @template T * @param {ID<T>} id * @param {T} value * @memberof ContainerClass */ bindValue<T> (id: ID<T>, value: T, opt: { scope?: string } = {}): this { const registion = new ValueRegistion(value, opt ? opt.scope : undefined) if (!opt.scope || this._map.hasName(opt.scope)) { this._map.set(id, registion) } else { const map = this._map.parent(opt.scope) if (map) { map.set(id, registion) } else { this._bindLazy(opt.scope, id, registion) } } return this } /** * bind lazy * * @template T * @param {ID<T>} id * @param {() => T} func * @param {BindOption} [opt] * @memberof ContainerClass */ bind<T> (id: ID<T>, creater: () => T, opt: BindOption = {}): this { const registion = new FactoryRegistion({ create: creater }, opt.singleton || false, opt.scope) if (!opt.scope || this._map.hasName(opt.scope)) { this._map.set(id, registion) } else { const map = this._map.parent(opt.scope) if (map) { map.set(id, registion) } else { this._bindLazy(opt.scope, id, registion) } } return this } bindFactory<T> (id: Constructor<T>, factory: Factory<T> | Constructor<Factory<T>>, opt: Partial<BindOption & { factorySingleton: boolean }> = {}): this { if (typeof factory === 'function') { // factory is constructor const factoryOpt = { scope: opt.scope, singleton: opt.factorySingleton === false ? false : true } this.bindClass(factory, factoryOpt) this.bind(id, () => { const f = this.get(factory, opt.scope) return f.create() }, opt) } else { this.bind(id, factory.create.bind(factory)) } return this } /** * * * @template T * @param {ID<T>} id * @param {Function} cls * @param {BindOption} [opt={}] * @memberof Container */ bindClassWithId (id: ID<any>, cls: Constructor<any>, opt: BindOption = {}): this { const registion = new ClassRegistion(cls, opt.singleton || false, opt.scope) if (!opt.scope || this._map.hasName(opt.scope)) { this._map.set(id, registion) } else { const map = this._map.parent(opt.scope) if (map) { map.set(id, registion) } else { this._bindLazy(opt.scope, id, registion) } } return this } /** * bind a class, ignore the decoractors * * @template T * @param {Constructor<T>} cls * @param {BindOption} [opt] * @memberof ContainerClass */ bindClass<T> (cls: Constructor<T>, opt: BindOption = {}): this { const registion = new ClassRegistion(cls, opt.singleton || false, opt.scope) if (!opt.scope || this._map.hasName(opt.scope)) { this._map.set(cls, registion) } else { const map = this._map.parent(opt.scope) if (map) { map.set(cls, registion) } else { this._bindLazy(opt.scope, cls, registion) } } return this } bindAlias<T> (id: ID<T>, toId: ID<T>, opt: Pick<BindOption, 'scope'> = {}): this { const registion = new AliasRegistion(toId) if (!opt.scope || this._map.hasName(opt.scope)) { this._map.set(id, registion) } else { const map = this._map.parent(opt.scope) if (map) { map.set(id, registion) } else { this._bindLazy(opt.scope, id, registion) } } return this } /** * auto bind a class with decorectors * * @param {Function} target * @memberof Container */ autoBind (target: Function): this { if (this._autoBinds.has(target)) { return this } this._autoBinds.add(target) const actions = getBindActions(target) || [] for (let action of actions) { action(target, this) } return this } /** * create name or alias a async scope * * @param {string} name * @memberof Container */ aliasScope (name: string) { if (this._map.parent(name)) { throw new Error('scope name should not be same with parent') } this._map.alias(name) if (this._lazyBinds[name]) { this._lazyBinds[name].forEach((reg, id) => { this._map.set(id, reg) }) } return this } /** * detect a scope * * @param {string} name * @returns * @memberof Container */ hasScope (name: string) { return this._map.hasName(name) || !!this._map.parent(name) } /** * fill a instance by it't prop decorectors * * @param {*} target * @param {string} [fromScope] * @memberof Container */ fill (target: any, fromScope?: string) { const injectProperties = getInjectMetas(target) for (let m of injectProperties) { const prop = m.property! if (target[prop]) { continue } let value !: any if (m.resolver) { value = m.resolver(this, m.scope || fromScope) } else if (m.id) { value = this.get(m.id, m.scope || fromScope) } else { const type = (Reflect as any).getMetadata("design:type", target, prop) value = this.get(type, m.scope || fromScope) } if (m.required && typeof value === 'undefined') { throw new InjectError('cant resolve depency,bug target prop is required') } target[prop] = value } } /** * get instance with id or class * * @template T * @param {(Function | Constructor<T>)} id * @param {string} [fromScope] * @returns {T} * @memberof Container */ get<T> (id: Function | Constructor<T>, fromScope?: string): T get<T=any> (id: string | symbol, fromScope?: string): T | undefined get (id: ID<any>, fromScope?: string): any { const map = fromScope ? this._map.closest(fromScope) : this._map let reg = map.get(id) if (reg) { return reg.getInstance(this, fromScope) } if (typeof id === 'function') { if (!this._autoBinds.has(id)) { this.autoBind(id) return this.get(id) } const reg = new ClassRegistion(id, false) return reg.getInstance(this, fromScope) } } /** * get the distance of scope which has the key * * @param {ID<any>} id * @returns * @memberof Container */ distance (id: ID<any>) { return this._map.distance(id) } protected _bindLazy (scope: string, id: any, registion: Registion<any>) { if (!this._lazyBinds[scope]) { this._lazyBinds[scope] = new Map() } this._lazyBinds[scope].set(id, registion) } }