UNPKG

proxydi

Version:

A typed hierarchical DI container that resolves circular dependencies via Proxy

591 lines (579 loc) 21.2 kB
const injectableClasses = {}; const constructorInjections = {}; function injectable(dependencyOrDependencies, autoInjecions) { return function (value, context) { if ((context === null || context === undefined ? undefined : context.kind) !== 'class') { throw new Error('@injectable decorator should decorate classes'); } const name = dependencyOrDependencies ? Array.isArray(dependencyOrDependencies) ? context.name : dependencyOrDependencies : context.name; const injectToConstructor = dependencyOrDependencies ? Array.isArray(dependencyOrDependencies) ? dependencyOrDependencies : autoInjecions : autoInjecions; if (injectableClasses[name]) { throw new Error(`ProxyDi has already regisered dependency ${String(name)} by @injectable`); } injectableClasses[name] = value; if (injectToConstructor) { constructorInjections[name] = injectToConstructor; } }; } function findInjectableId(injectable) { for (const [id, DependencyClass] of Object.entries(injectableClasses)) { if (DependencyClass === injectable) { return id; } } throw new Error(`Class is not @injectable: ${injectable.name}`); } const INJECTIONS = Symbol('injections'); /** * This symbol constant defines a property name. * This property is present in each dependency instance that was registered in ProxyDiContainer. * The property stores the dependency identifier that should be used to resolve dependency from the container where it was registered. */ const DEPENDENCY_ID = Symbol('DependencyId'); /** * This symbol constant defines a property name. * This property is present in each dependency instance that was registered in ProxyDiContainer. * The property stores a reference to the ProxyDiContainer in which the dependency was registered. */ const PROXYDI_CONTAINER = Symbol('proxyDiContainer'); const ON_CONTAINERIZED = Symbol('onContainerized'); const IS_INJECTION_PROXY = Symbol('isInjectionProxy'); const INJECTION_OWNER = Symbol('injectionOwner'); const IS_INSTANCE_PROXY = Symbol('isInstanceProxy'); /** * Registers an injection for dependency injection. * * @param dependencyId - Optional dependecy identifier. If omitted, the property name is used. * @returns A decorator function for class fields. * * The decorated field will receive its dependency from the same container as the injection owner. */ const inject = (dependencyId) => { return function (_value, context) { if ((context === null || context === undefined ? undefined : context.kind) === 'field') { const id = dependencyId ? typeof dependencyId === 'function' ? findInjectableId(dependencyId) : dependencyId : context.name; const injection = { property: context.name, dependencyId: id, set: context.access.set, }; context.addInitializer(function () { if (!this[INJECTIONS]) { this[INJECTIONS] = {}; } this[INJECTIONS][injection.property] = injection; }); } else { throw new Error('@inject decorator should decorate fields'); } }; }; const DEFAULT_SETTINGS = { allowRegisterAnything: false, allowRewriteDependencies: false, resolveInContainerContext: false, }; var _a; class InjectionProxy { constructor(onwer, container) { this[_a] = true; this[INJECTION_OWNER] = onwer; this[PROXYDI_CONTAINER] = container; } } _a = IS_INJECTION_PROXY; const makeInjectionProxy = (injection, injectionOwner, container) => { function getDependency() { if (container.isKnown(injection.dependencyId)) { const dependency = container.resolve(injection.dependencyId); if (!container.settings.allowRewriteDependencies) { injection.set(injectionOwner, dependency); } return dependency; } else { throw new Error(`Unknown dependency: ${String(injection.dependencyId)}`); } } return new Proxy(new InjectionProxy(injectionOwner, container), { get: function (target, prop, receiver) { if (target[prop]) { return target[prop]; } const dependency = getDependency(); return Reflect.get(dependency, prop, receiver); }, set: function (target, prop, value) { const dependency = getDependency(); return Reflect.set(dependency, prop, value); }, has: function (target, prop) { const dependency = getDependency(); return Reflect.has(dependency, prop); }, }); }; function makeDependencyProxy(dependency) { const injectionValues = {}; return new Proxy(dependency, { get: function (target, prop, receiver) { if (prop === IS_INSTANCE_PROXY) { return true; } if (injectionValues[prop]) { return injectionValues[prop]; } return Reflect.get(target, prop, receiver); }, set: function (target, prop, value) { injectionValues[prop] = value; return Reflect.set(target, prop, value); }, }); } function makeConstructorDependencyProxy(container, dependencyId) { function getDependency() { if (container.isKnown(dependencyId)) { const dependency = container.resolve(dependencyId); return dependency; } else { throw new Error(`Unknown dependency: ${String(dependencyId)}`); } } return new Proxy({}, { get: function (target, prop, receiver) { const dependency = getDependency(); return Reflect.get(dependency, prop, receiver); }, set: function (_target, prop, value) { const dependency = getDependency(); return Reflect.set(dependency, prop, value); }, has: function (_target, prop) { const dependency = getDependency(); return Reflect.has(dependency, prop); }, }); } const middlewaresClasses = {}; function middleware() { return function (value, context) { if ((context === null || context === undefined ? undefined : context.kind) !== 'class') { throw new Error('@middleware decorator should decorate classes'); } const name = context.name; if (middlewaresClasses[name]) { throw new Error(`ProxyDi has already regisered middleware ${String(name)} by @middleware`); } middlewaresClasses[name] = value; }; } class MiddlewareManager { constructor(parent) { this.parent = parent; this.handlers = { register: [], remove: [], resolve: [], }; } add(middleware) { if (isRegistrator(middleware)) { middleware.onRegister && this.on('register', middleware.onRegister); } if (isRemover(middleware)) { middleware.onRemove && this.on('remove', middleware.onRemove); } if (isResolver(middleware)) { middleware.onResolve && this.on('resolve', middleware.onResolve); } } remove(middleware) { if (isRegistrator(middleware)) { middleware.onRegister && this.off('register', middleware.onRegister); } if (isRemover(middleware)) { middleware.onRemove && this.off('remove', middleware.onRemove); } if (isResolver(middleware)) { middleware.onResolve && this.off('resolve', middleware.onResolve); } } on(event, listener) { this.handlers[event].push(listener); } onRegister(context) { var _a; this.handlers.register.forEach((listener) => listener(context)); (_a = this.parent) === null || _a === undefined ? undefined : _a.onRegister(context); } onRemove(context) { var _a; this.handlers.remove.forEach((listener) => listener(context)); (_a = this.parent) === null || _a === undefined ? undefined : _a.onRemove(context); } onResolve(context) { let result = context; this.handlers.resolve.forEach((listener) => { result = listener(result); }); return result; } off(event, listener) { const index = this.handlers[event].indexOf(listener); if (index !== -1) { this.handlers[event].splice(index, 1); } } } function isRegistrator(middleware) { return !!middleware.onRegister; } function isRemover(middleware) { return !!middleware.onRemove; } function isResolver(middleware) { return !!middleware.onResolve; } /** * A dependency injection container */ class ProxyDiContainer { /** * Creates a new instance of ProxyDiContainer. * @param settings Optional container settings to override defaults. * @param parent Optional parent container. */ constructor(settings, parent) { this._children = {}; /** * Holds dependency instances registered particular in this container. */ this.dependencies = {}; /** * Holds proxies for dependencies registered in parent containers to provide for it dependencies from this container */ this.inContextProxies = {}; this.resolveImpl = (dependencyId) => { const proxy = this.inContextProxies[dependencyId]; if (proxy) { return proxy; } const instance = this.findDependency(dependencyId); if (instance) { if (instance[PROXYDI_CONTAINER] !== this && typeof instance === 'object' && this.settings.resolveInContainerContext) { const proxy = makeDependencyProxy(instance); this.injectDependenciesTo(proxy); this.inContextProxies[dependencyId] = proxy; return proxy; } return instance; } const InjectableClass = injectableClasses[dependencyId]; return this.register(InjectableClass, dependencyId); }; this.id = ProxyDiContainer.idCounter++; this.middlewareManager = new MiddlewareManager(parent === null || parent === undefined ? undefined : parent.middlewareManager); if (parent) { this.parent = parent; this.parent.addChild(this); } this.settings = Object.assign(Object.assign({}, DEFAULT_SETTINGS), settings); } registerMiddleware(middleware) { this.middlewareManager.add(middleware); } removeMiddleware(middleware) { this.middlewareManager.remove(middleware); } register(dependency, dependecyId) { var _a; let id = dependecyId; if (!id) { if (typeof dependency === 'function') { try { id = findInjectableId(dependency); } catch (_b) { id = dependency.name; } } } if (this.dependencies[id]) { if (!this.settings.allowRewriteDependencies) { throw new Error(`ProxyDi already has dependency for ${String(id)}`); } } let instance; const isClass = typeof dependency === 'function'; if (isClass) { instance = this.createInstance(dependency, id); } else { instance = dependency; } const isObject = typeof instance === 'object'; if (!isObject && !this.settings.allowRegisterAnything) { throw new Error(`Can't register as dependency (allowRegisterAnything is off for this contatiner): ${instance}`); } if (isObject) { instance[PROXYDI_CONTAINER] = this; instance[DEPENDENCY_ID] = id; instance[ON_CONTAINERIZED] && instance[ON_CONTAINERIZED](this); } this.injectDependenciesTo(instance); this.dependencies[id] = instance; const constructorName = (_a = instance.constructor) === null || _a === undefined ? undefined : _a.name; if (constructorName && middlewaresClasses[constructorName]) { this.middlewareManager.add(instance); } let context = { container: this, dependencyId: id, dependency: instance, }; this.middlewareManager.onRegister(context); return instance; } createInstance(Dependency, dependencyId) { const paramIds = constructorInjections[dependencyId] || []; const params = []; for (const id of paramIds) { const param = makeConstructorDependencyProxy(this, id); params.push(param); } return new Dependency(...params); } /** * Checks if a dependency with the given ID is known to the container or its ancestors which means that it can be resolved by this container * @param dependencyId The identifier of the dependency. * @returns True if the dependency is known, false otherwise. */ isKnown(dependencyId) { return !!(this.inContextProxies[dependencyId] || this.dependencies[dependencyId] || (this.parent && this.parent.isKnown(dependencyId)) || injectableClasses[dependencyId]); } resolve(dependency) { if (typeof dependency === 'function') { let id; try { id = findInjectableId(dependency); } catch (_a) { id = dependency.name; } return this.resolve(id); } if (!this.isKnown(dependency)) { throw new Error(`Can't resolve unknown dependency: ${String(dependency)}`); } let context = { container: this, dependencyId: dependency, dependency: this.resolveImpl(dependency), }; context = this.middlewareManager.onResolve(context); return context.dependency; } /** * Injects dependencies to the given object based on its defined injections metadata. Does not affect the container. * @param injectionsOwner The object to inject dependencies into. */ injectDependenciesTo(injectionsOwner) { const dependencyInjects = injectionsOwner[INJECTIONS] || {}; Object.values(dependencyInjects).forEach((injection) => { const dependencyProxy = makeInjectionProxy(injection, injectionsOwner, this); injection.set(injectionsOwner, dependencyProxy); }); } /** * Creates instances for all injectable classes and registers them in this container. * @returns This container to allow use along with constructor. */ registerInjectables() { for (const [dependencyId, InjectableClass] of Object.entries(injectableClasses)) { this.register(InjectableClass, dependencyId); } return this; } /** * Finalizes dependency injections, prevents further rewriting of dependencies, * and recursively bakes injections for child containers. */ bakeInjections() { for (const dependency of Object.values(this.dependencies)) { const dependencyInjects = dependency[INJECTIONS] || {}; Object.values(dependencyInjects).forEach((inject) => { const value = this.resolve(inject.dependencyId); inject.set(dependency, value); }); } this.settings.allowRewriteDependencies = false; for (const child of Object.values(this._children)) { child.bakeInjections(); } } /** * Creates a child container that inherits settings and dependencies from this container. * @returns A new child instance of ProxyDiContainer. */ createChildContainer() { return new ProxyDiContainer(this.settings, this); } /** * Removes a given dependency from the container using either the dependency instance or its ID. * @param dependencyOrId The dependency instance or dependency identifier to remove. */ remove(dependencyOrId) { var _a; const id = isDependency(dependencyOrId) ? dependencyOrId[DEPENDENCY_ID] : dependencyOrId; const dependency = this.dependencies[id]; if (dependency) { const constructorName = (_a = dependency.constructor) === null || _a === undefined ? undefined : _a.name; if (constructorName && middlewaresClasses[constructorName]) { this.middlewareManager.remove(dependency); } const dependencyInjects = dependency[INJECTIONS] ? dependency[INJECTIONS] : {}; Object.values(dependencyInjects).forEach((inject) => { inject.set(dependency, undefined); }); delete dependency[DEPENDENCY_ID]; delete this.dependencies[id]; this.middlewareManager.onRemove({ container: this, dependencyId: id, dependency, }); } } /** * Destroys the container by removing all dependencies, * recursively destroying child containers and removing itself from its parent. */ destroy() { const allDependencies = Object.values(this.dependencies); for (const dependency of allDependencies) { this.remove(dependency); } this.dependencies = {}; for (const child of Object.values(this._children)) { child.destroy(); } this._children = {}; if (this.parent) { this.parent.removeChild(this.id); this.parent = undefined; } } /** * Recursively finds a dependency by its ID from this container or its parent. * @param dependencyId The identifier of the dependency to find. * @returns The dependency if found, otherwise undefined. */ findDependency(dependencyId) { const dependency = this.dependencies[dependencyId]; if (!dependency && this.parent) { const parentDependency = this.parent.findDependency(dependencyId); return parentDependency; } return dependency; } /** * All direct descendants of this container */ get children() { return Object.values(this._children); } /** * * @param id Unique identifier of container * @returns */ getChild(id) { const child = this._children[id]; if (!child) { throw new Error(`Unknown ProxyDiContainer child ID: ${id}`); } return child; } /** * Registers a child container to this container. * @param child The child container to add. * @throws Error if a child with the same ID already exists. */ addChild(child) { if (this._children[child.id]) { throw new Error(`ProxyDi already has child with id ${child.id}`); } this._children[child.id] = child; } /** * Removes a child container by its ID. * @param id The identifier of the child container to remove. */ removeChild(id) { const child = this._children[id]; if (child) { delete this._children[id]; } } } /** * Static counter used to assign unique IDs to containers. */ ProxyDiContainer.idCounter = 0; /** * Helper function to determine if the provided argument is a dependency instance. * @param dependencyOrId The dependency instance or dependency identifier. * @returns True if the argument is a dependency instance, false otherwise. */ function isDependency(dependencyOrId) { return (typeof dependencyOrId === 'object' && !!dependencyOrId[DEPENDENCY_ID]); } function resolveAll(instance, dependencyId) { if (typeof dependencyId === 'function') { const id = findInjectableId(dependencyId); return resolveAll(instance, id); } const container = instance[PROXYDI_CONTAINER]; if (!container) { throw new Error('Instance is not registered in any container'); } return recursiveResolveAll(container, dependencyId); } function recursiveResolveAll(container, dependencyId) { let all = container.isKnown(dependencyId) ? [container.resolve(dependencyId)] : []; for (const child of container.children) { const allChild = recursiveResolveAll(child, dependencyId); all = all.concat(allChild); } return all; } export { DEPENDENCY_ID, ON_CONTAINERIZED, PROXYDI_CONTAINER, ProxyDiContainer, inject, injectable, middleware, resolveAll };