UNPKG

typedi

Version:

Dependency injection for TypeScript.

318 lines 15.1 kB
var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __spreadArrays = (this && this.__spreadArrays) || function () { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; return r; }; 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. */ var ContainerInstance = /** @class */ (function () { function ContainerInstance(id) { /** All registered services in the container. */ this.services = []; this.id = id; } ContainerInstance.prototype.has = function (identifier) { return !!this.findService(identifier); }; ContainerInstance.prototype.get = function (identifier) { var globalContainer = Container.of(undefined); var globalService = globalContainer.findService(identifier); var 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) { var clonedService = __assign({}, 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); var value = this.getServiceValue(clonedService); this.set(__assign(__assign({}, clonedService), { value: value })); return value; } if (globalService) return this.getServiceValue(globalService); throw new ServiceNotFoundError(identifier); }; ContainerInstance.prototype.getMany = function (identifier) { var _this = this; return this.findAllServices(identifier).map(function (service) { return _this.getServiceValue(service); }); }; ContainerInstance.prototype.set = function (identifierOrServiceMetadata, value) { var _this = this; if (identifierOrServiceMetadata instanceof Array) { identifierOrServiceMetadata.forEach(function (data) { return _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, }); } var newService = __assign({ id: new Token('UNREACHABLE'), type: null, factory: undefined, value: EMPTY_VALUE, global: false, multiple: false, eager: false, transient: false }, identifierOrServiceMetadata); var 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. */ ContainerInstance.prototype.remove = function (identifierOrIdentifierArray) { var _this = this; if (Array.isArray(identifierOrIdentifierArray)) { identifierOrIdentifierArray.forEach(function (id) { return _this.remove(id); }); } else { this.services = this.services.filter(function (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. */ ContainerInstance.prototype.reset = function (options) { var _this = this; if (options === void 0) { options = { strategy: 'resetValue' }; } switch (options.strategy) { case 'resetValue': this.services.forEach(function (service) { return _this.destroyServiceInstance(service); }); break; case 'resetServices': this.services.forEach(function (service) { return _this.destroyServiceInstance(service); }); this.services = []; break; default: throw new Error('Received invalid reset strategy.'); } return this; }; /** * Returns all services registered with the given identifier. */ ContainerInstance.prototype.findAllServices = function (identifier) { return this.services.filter(function (service) { return service.id === identifier; }); }; /** * Finds registered service in the with a given service identifier. */ ContainerInstance.prototype.findService = function (identifier) { return this.services.find(function (service) { return 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 */ ContainerInstance.prototype.getServiceValue = function (serviceMetadata) { var _a; var 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) { var factoryInstance = void 0; 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) { var constructableTargetType = serviceMetadata.type; // setup constructor parameters for a newly initialized service var paramTypes = ((_a = Reflect) === null || _a === void 0 ? void 0 : _a.getMetadata('design:paramtypes', constructableTargetType)) || []; var 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.bind.apply(constructableTargetType, __spreadArrays([void 0], 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. */ ContainerInstance.prototype.initializeParams = function (target, paramTypes) { var _this = this; return paramTypes.map(function (paramType, index) { var paramHandler = Container.handlers.find(function (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. */ ContainerInstance.prototype.isPrimitiveParamType = function (paramTypeName) { return ['string', 'boolean', 'number', 'object'].includes(paramTypeName.toLowerCase()); }; /** * Applies all registered handlers on a given target class. */ ContainerInstance.prototype.applyPropertyHandlers = function (target, instance) { var _this = this; Container.handlers.forEach(function (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 */ ContainerInstance.prototype.destroyServiceInstance = function (serviceMetadata, force) { if (force === void 0) { force = false; } /** We reset value only if we can re-create it (aka type or factory exists). */ var 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; } }; return ContainerInstance; }()); export { ContainerInstance }; //# sourceMappingURL=container-instance.class.js.map