UNPKG

typedi

Version:

Dependency injection for TypeScript.

300 lines 13.4 kB
import { Container } from './container.class'; import { ServiceNotFoundError } from './error/service-not-found.error'; import { CannotInstantiateValueError } from './error/cannot-instantiate-value.error'; import { Token } from './token.class'; import { EMPTY_VALUE } from './empty.const'; /** * TypeDI can have multiple containers. * One container is ContainerInstance. */ export class ContainerInstance { constructor(id) { /** All registered services in the container. */ this.services = []; this.id = id; } has(identifier) { return !!this.findService(identifier); } get(identifier) { const globalContainer = Container.of(undefined); const globalService = globalContainer.findService(identifier); const scopedService = this.findService(identifier); if (globalService && globalService.global === true) return this.getServiceValue(globalService); if (scopedService) return this.getServiceValue(scopedService); /** If it's the first time requested in the child container we load it from parent and set it. */ if (globalService && this !== globalContainer) { const clonedService = { ...globalService }; clonedService.value = EMPTY_VALUE; /** * We need to immediately set the empty value from the root container * to prevent infinite lookup in cyclic dependencies. */ this.set(clonedService); const value = this.getServiceValue(clonedService); this.set({ ...clonedService, value }); return value; } if (globalService) return this.getServiceValue(globalService); throw new ServiceNotFoundError(identifier); } getMany(identifier) { return this.findAllServices(identifier).map(service => this.getServiceValue(service)); } set(identifierOrServiceMetadata, value) { if (identifierOrServiceMetadata instanceof Array) { identifierOrServiceMetadata.forEach(data => this.set(data)); return this; } if (typeof identifierOrServiceMetadata === 'string' || identifierOrServiceMetadata instanceof Token) { return this.set({ id: identifierOrServiceMetadata, type: null, value: value, factory: undefined, global: false, multiple: false, eager: false, transient: false, }); } if (typeof identifierOrServiceMetadata === 'function') { return this.set({ id: identifierOrServiceMetadata, // TODO: remove explicit casting type: identifierOrServiceMetadata, value: value, factory: undefined, global: false, multiple: false, eager: false, transient: false, }); } const newService = { id: new Token('UNREACHABLE'), type: null, factory: undefined, value: EMPTY_VALUE, global: false, multiple: false, eager: false, transient: false, ...identifierOrServiceMetadata, }; const service = this.findService(newService.id); if (service && service.multiple !== true) { Object.assign(service, newService); } else { this.services.push(newService); } if (newService.eager) { this.get(newService.id); } return this; } /** * Removes services with a given service identifiers. */ remove(identifierOrIdentifierArray) { if (Array.isArray(identifierOrIdentifierArray)) { identifierOrIdentifierArray.forEach(id => this.remove(id)); } else { this.services = this.services.filter(service => { if (service.id === identifierOrIdentifierArray) { this.destroyServiceInstance(service); return false; } return true; }); } return this; } /** * Completely resets the container by removing all previously registered services from it. */ reset(options = { strategy: 'resetValue' }) { switch (options.strategy) { case 'resetValue': this.services.forEach(service => this.destroyServiceInstance(service)); break; case 'resetServices': this.services.forEach(service => this.destroyServiceInstance(service)); this.services = []; break; default: throw new Error('Received invalid reset strategy.'); } return this; } /** * Returns all services registered with the given identifier. */ findAllServices(identifier) { return this.services.filter(service => service.id === identifier); } /** * Finds registered service in the with a given service identifier. */ findService(identifier) { return this.services.find(service => service.id === identifier); } /** * Gets the value belonging to `serviceMetadata.id`. * * - if `serviceMetadata.value` is already set it is immediately returned * - otherwise the requested type is resolved to the value saved to `serviceMetadata.value` and returned */ getServiceValue(serviceMetadata) { var _a; let value = EMPTY_VALUE; /** * If the service value has been set to anything prior to this call we return that value. * NOTE: This part builds on the assumption that transient dependencies has no value set ever. */ if (serviceMetadata.value !== EMPTY_VALUE) { return serviceMetadata.value; } /** If both factory and type is missing, we cannot resolve the requested ID. */ if (!serviceMetadata.factory && !serviceMetadata.type) { throw new CannotInstantiateValueError(serviceMetadata.id); } /** * If a factory is defined it takes priority over creating an instance via `new`. * The return value of the factory is not checked, we believe by design that the user knows what he/she is doing. */ if (serviceMetadata.factory) { /** * If we received the factory in the [Constructable<Factory>, "functionName"] format, we need to create the * factory first and then call the specified function on it. */ if (serviceMetadata.factory instanceof Array) { let factoryInstance; try { /** Try to get the factory from TypeDI first, if failed, fall back to simply initiating the class. */ factoryInstance = this.get(serviceMetadata.factory[0]); } catch (error) { if (error instanceof ServiceNotFoundError) { factoryInstance = new serviceMetadata.factory[0](); } else { throw error; } } value = factoryInstance[serviceMetadata.factory[1]](this, serviceMetadata.id); } else { /** If only a simple function was provided we simply call it. */ value = serviceMetadata.factory(this, serviceMetadata.id); } } /** * If no factory was provided and only then, we create the instance from the type if it was set. */ if (!serviceMetadata.factory && serviceMetadata.type) { const constructableTargetType = serviceMetadata.type; // setup constructor parameters for a newly initialized service const paramTypes = ((_a = Reflect) === null || _a === void 0 ? void 0 : _a.getMetadata('design:paramtypes', constructableTargetType)) || []; const params = this.initializeParams(constructableTargetType, paramTypes); // "extra feature" - always pass container instance as the last argument to the service function // this allows us to support javascript where we don't have decorators and emitted metadata about dependencies // need to be injected, and user can use provided container to get instances he needs params.push(this); value = new constructableTargetType(...params); // TODO: Calling this here, leads to infinite loop, because @Inject decorator registerds a handler // TODO: which calls Container.get, which will check if the requested type has a value set and if not // TODO: it will start the instantiation process over. So this is currently called outside of the if branch // TODO: after the current value has been assigned to the serviceMetadata. // this.applyPropertyHandlers(constructableTargetType, value as Constructable<unknown>); } /** If this is not a transient service, and we resolved something, then we set it as the value. */ if (!serviceMetadata.transient && value !== EMPTY_VALUE) { serviceMetadata.value = value; } if (value === EMPTY_VALUE) { /** This branch should never execute, but better to be safe than sorry. */ throw new CannotInstantiateValueError(serviceMetadata.id); } if (serviceMetadata.type) { this.applyPropertyHandlers(serviceMetadata.type, value); } return value; } /** * Initializes all parameter types for a given target service class. */ initializeParams(target, paramTypes) { return paramTypes.map((paramType, index) => { const paramHandler = Container.handlers.find(handler => { /** * @Inject()-ed values are stored as parameter handlers and they reference their target * when created. So when a class is extended the @Inject()-ed values are not inherited * because the handler still points to the old object only. * * As a quick fix a single level parent lookup is added via `Object.getPrototypeOf(target)`, * however this should be updated to a more robust solution. * * TODO: Add proper inheritance handling: either copy the handlers when a class is registered what * TODO: has it's parent already registered as dependency or make the lookup search up to the base Object. */ return ((handler.object === target || handler.object === Object.getPrototypeOf(target)) && handler.index === index); }); if (paramHandler) return paramHandler.value(this); if (paramType && paramType.name && !this.isPrimitiveParamType(paramType.name)) { return this.get(paramType); } return undefined; }); } /** * Checks if given parameter type is primitive type or not. */ isPrimitiveParamType(paramTypeName) { return ['string', 'boolean', 'number', 'object'].includes(paramTypeName.toLowerCase()); } /** * Applies all registered handlers on a given target class. */ applyPropertyHandlers(target, instance) { Container.handlers.forEach(handler => { if (typeof handler.index === 'number') return; if (handler.object.constructor !== target && !(target.prototype instanceof handler.object.constructor)) return; if (handler.propertyName) { instance[handler.propertyName] = handler.value(this); } }); } /** * Checks if the given service metadata contains a destroyable service instance and destroys it in place. If the service * contains a callable function named `destroy` it is called but not awaited and the return value is ignored.. * * @param serviceMetadata the service metadata containing the instance to destroy * @param force when true the service will be always destroyed even if it's cannot be re-created */ destroyServiceInstance(serviceMetadata, force = false) { /** We reset value only if we can re-create it (aka type or factory exists). */ const shouldResetValue = force || !!serviceMetadata.type || !!serviceMetadata.factory; if (shouldResetValue) { /** If we wound a function named destroy we call it without any params. */ if (typeof (serviceMetadata === null || serviceMetadata === void 0 ? void 0 : serviceMetadata.value)['destroy'] === 'function') { try { serviceMetadata.value.destroy(); } catch (error) { /** We simply ignore the errors from the destroy function. */ } } serviceMetadata.value = EMPTY_VALUE; } } } //# sourceMappingURL=container-instance.class.js.map